├── .circleci └── config.yml ├── .gitignore ├── .shellspec ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cli.sh ├── site-template-example.lokl ├── spec ├── cli_spec.sh ├── spec_helper.sh └── test-data │ ├── docker-inspect-port-mapping-0-0-0-0-4363 │ └── docker-ps-a-output-mixed-001 └── test.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | shellcheck: circleci/shellcheck@1.3.16 4 | jobs: 5 | shellspec: 6 | docker: 7 | - image: cimg/base:stable 8 | steps: 9 | - checkout 10 | - run: curl -fsSL https://git.io/shellspec | sh -s -- -y 11 | - run: sudo ln -s ${HOME}/.local/lib/shellspec/shellspec /usr/local/bin/shellspec 12 | - run: sudo apt update -y 13 | - run: sudo apt install -y kcov 14 | - run: export TERM=${TERM:-dumb} && shellspec --kcov 15 | - store_artifacts: 16 | path: coverage 17 | 18 | workflows: 19 | codequality: 20 | jobs: 21 | - shellcheck/check: 22 | exclude: ./git/* 23 | - shellspec 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | -------------------------------------------------------------------------------- /.shellspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --kcov-options "--include-pattern=cli.sh" 3 | 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Lokl CLI 5.0.0 2 | 3 | - mount volumes from host via site templates 4 | - fix for running cmds with no sites. Thx @yoannsark! 5 | - renamed "go" script to "cli" 6 | 7 | ## Lokl Go 4.0 8 | 9 | - easily watch error logs 10 | 11 | ## Lokl Go 3.0 12 | 13 | - support for Lokl 0.0.19 14 | - open WordPress admin (l/p: admin/admin) 15 | - open phpMyAdmin 16 | 17 | ## Lokl Go 2.0 18 | 19 | - allow to stop/delete containers 20 | - auto-launch stopped containers before use 21 | - check curl/Docker requirements 22 | 23 | ## Lokl 1.0 24 | 25 | - site backups 26 | - SSH into containers 27 | - open site in browser 28 | 29 | ## Lokl 0.1 30 | 31 | - create containers 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lokl-cli 2 | ======== 3 | 4 | Interactive wizard or noninteractive script for launching and managing your [lokl](https://lokl.dev) WordPress sites. 5 | 6 | Usage 7 | ===== 8 | 9 | ### macOS, Linux, Windows 10 | 11 | The simplest way to get started, paste the following into a terminal to launch Lokl's interactive wizard: 12 | 13 | `sh -c "$(curl -sSl 'https://lokl.dev/cli-5.0.0')"` 14 | 15 | ### Site templates 16 | 17 | From version 5.0.0, Lokl now supports site template files, which, if present, Lokl will allow you to choose as a template for your new site. They're totally optional, Lokl runs just fine without them. 18 | 19 | Currently, these allow specifying directories from your host machine to mount within your Lokl site's container. This makes it easier for those editing plugins/themes/site files on their local computer and having the changes apply immediately within their Lokl site. 20 | 21 | Future enhancements to this templating will allow for things like specifying different sets of plugins/themes to auto-install in new Lokl sites. 22 | 23 | An example site template file is located within this repository, named `site-template-example.lokl`. There are comments in this template, describing how to use it, also described here: 24 | 25 | - make a `templates` directory inside a `.lokl` directory in your `$HOME` folder. 26 | 27 | ie, on macOS, this would be `/Users/leon/.lokl/templates` 28 | 29 | - copy the example `site-template-example.lokl` template from this repository into that Lokl templates folder, naming it something descriptive 30 | - edit the volumes section to specify which directories you want to be shared from your host operating system to within the container running your Lokl site 31 | - when you next run the Lokl CLI wizard to create a site, you'll be presented with a list of your templates to choose from 32 | 33 | #### Programmatic usage 34 | 35 | If you're familiar with Docker and bash, you can read through the source code of this repository and the [lokl](https://github.com/leonstafford/lokl)'s to see how I provision and control Lokl. 36 | 37 | Any docs I write about that will be quickly out of date, so please refer to the code and ask me any specific questions. 38 | 39 | 40 | Build status 41 | ============ 42 | 43 | [![CircleCI](https://circleci.com/gh/leonstafford/lokl-cli.svg?style=svg)](https://circleci.com/gh/leonstafford/lokl-cli) 44 | 45 | Testing 46 | ======= 47 | 48 | - `shellcheck` 49 | - `shellspec` 50 | 51 | With code coverage report: 52 | 53 | - `shellspec --kcov` 54 | 55 | For convenience, you can run `sh test.sh`. 56 | 57 | CircleCI config runs both of these commands. 58 | 59 | Debug log 60 | ========= 61 | 62 | To aid in development or user support, lokl-cli appends to a log file 63 | in your system's temp directory, which can be followed by: 64 | 65 | `touch /tmp/lokldebuglog && tail -F /tmp/lokldebuglog` 66 | 67 | Style Guide 68 | =========== 69 | 70 | In lieu of an automatic beautifier, refer to [Google Shellguide](https://google.github.io/styleguide/shellguide.html) if unsure. If you know of something like PHPCodeSniffer and PHPCodeBeautifier to compliment ShellCheck, please let me know! 71 | 72 | -------------------------------------------------------------------------------- /cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # lokl-cli: Lokl WordPress site launcher & manager 4 | # 5 | # Allows users to easily spin-up and manage new Lokl WordPress instances 6 | # 7 | # License: The Unlicense, https://unlicense.org 8 | # 9 | # Usage: execute this script from the project root 10 | # 11 | # run from internet: 12 | # 13 | # $ sh -c "$(curl -sSl 'https://lokl.dev/go?v=4')" 14 | # 15 | # run locally: 16 | # 17 | # $ sh cli.sh 18 | # 19 | # to skip the wizard, call the script with vars set: 20 | # 21 | # lokl_php_ver=php8-5.0.0 \ 22 | # lokl_site_name=bananapants \ 23 | # lokl_site_port=4444 \ 24 | # sh cli.sh 25 | 26 | lokl_log() { 27 | timestamp="$(date '+%H:%M:%S')" 28 | echo "$timestamp: $1" >> /tmp/lokldebuglog 29 | } 30 | 31 | set_docker_tag() { 32 | # shellcheck disable=SC2154 33 | if [ "$lokl_php_ver" ]; then 34 | echo "$lokl_php_ver-$LOKL_RELEASE_VERSION" 35 | else 36 | echo "php8-$LOKL_RELEASE_VERSION" 37 | fi 38 | } 39 | 40 | set_site_name() { 41 | # shellcheck disable=SC2154 42 | if [ "$lokl_site_name" ]; then 43 | echo "$lokl_site_name" 44 | else 45 | echo "" 46 | fi 47 | } 48 | 49 | set_site_port() { 50 | # shellcheck disable=SC2154 51 | if [ "$lokl_site_port" ]; then 52 | echo "$lokl_site_port" 53 | else 54 | echo "" 55 | fi 56 | } 57 | 58 | set_curl_timeout_max_attempts() { 59 | if [ "$1" = "1" ]; then 60 | echo 2 61 | else 62 | echo 12 63 | fi 64 | } 65 | 66 | set_site_poll_sleep_duration() { 67 | if [ "$1" = "1" ]; then 68 | echo 0.1 69 | else 70 | echo 5 71 | fi 72 | } 73 | 74 | main_menu() { 75 | clear 76 | echo "" 77 | echo "================================================" 78 | echo " Lokl launcher & management script " 79 | echo "" 80 | echo " https://lokl.dev" 81 | echo "" 82 | echo "================================================" 83 | echo " Press (Ctrl) and (c) keys to exit anytime" 84 | echo "------------------------------------------------" 85 | echo "" 86 | echo "c) Create new Lokl WordPress site" 87 | echo "m) Manage my existing Lokl sites" 88 | echo "" 89 | echo "q) Quit this menu" 90 | echo "" 91 | echo "" 92 | echo "Please type (c), (m) or (q) and the Enter key: " 93 | echo "" 94 | read -r main_menu_choice 95 | 96 | if [ "$main_menu_choice" != "${main_menu_choice#[cmq]}" ]; then 97 | case $main_menu_choice in 98 | c|C) create_site_choose_name ;; 99 | m|M) manage_sites_menu ;; 100 | q|Q) exit 0 ;; 101 | esac 102 | 103 | else 104 | main_menu 105 | fi 106 | } 107 | 108 | test_core_capabilities() { 109 | clear 110 | echo "" 111 | echo "Checking system requirements... " 112 | echo "" 113 | test_docker_available 114 | test_curl_available 115 | } 116 | 117 | 118 | test_curl_available() { 119 | if ! command -v curl > /dev/null 120 | then 121 | echo "cURL doesn't seem to be installed." 122 | exit 1 123 | fi 124 | } 125 | 126 | test_docker_available() { 127 | if ! docker run --rm hello-world > /dev/null 2>&1 128 | then 129 | echo "Docker doesn't seem to be running or suitably configured for Lokl" 130 | exit 1 131 | fi 132 | } 133 | 134 | create_site_choose_php_version() { 135 | # TODO: skip choice is version has been defined in a Lokl site template 136 | 137 | clear 138 | echo "" 139 | echo "Choose the PHP version for your new Lokl WordPress site. " 140 | echo "" 141 | echo "" 142 | echo "8) PHP 8.0 (recommended)" 143 | echo "7) PHP 7.4" 144 | echo "" 145 | echo "" 146 | echo "Type 8 or 7, then the Enter key: " 147 | echo "" 148 | 149 | read -r create_site_php_choice 150 | 151 | lokl_log "User input desired php version: $create_site_php_choice" 152 | 153 | if [ "$create_site_php_choice" -eq 8 ]; then 154 | LOKL_DOCKER_TAG="php8-$LOKL_RELEASE_VERSION" 155 | elif [ "$create_site_php_choice" -eq 7 ]; then 156 | LOKL_DOCKER_TAG="php7-$LOKL_RELEASE_VERSION" 157 | else 158 | create_site_choose_php_version 159 | fi 160 | 161 | create_wordpress_docker_container 162 | } 163 | 164 | create_site_choose_name() { 165 | # purposely put this after main_menu() to allow users to see what the wizard 166 | # is like before reporting any detected inabilities to create a site, which 167 | # also requires a little delay. ie, UX over logical function flow 168 | test_core_capabilities 169 | clear 170 | echo "" 171 | echo "Choose a name for your new Lokl WordPress site. " 172 | echo "" 173 | echo "Please use letters, numbers and hyphens " 174 | echo "" 175 | echo "ie, portfolio" 176 | echo "" 177 | echo "" 178 | echo "Type your site name, then the Enter key: " 179 | echo "" 180 | 181 | read -r create_site_name_choice 182 | 183 | lokl_log "User input desired sitename: $create_site_name_choice" 184 | 185 | LOKL_NAME="$(sanitize_site_name "$create_site_name_choice")" 186 | 187 | lokl_log "Sanitized sitename:: $LOKL_NAME" 188 | 189 | # check name is not empty 190 | if [ "$LOKL_NAME" = "" ]; then 191 | if [ "$LOKL_TEST_MODE" ]; then 192 | lokl_log "Empty or invalid site name entered" 193 | # early exit when testing for easier assertion 194 | exit 1 195 | fi 196 | 197 | # re-ask for name entry if input was invalid 198 | create_site_choose_name 199 | fi 200 | 201 | lokl_log "User input site name: $LOKL_NAME" 202 | 203 | choose_lokl_site_template 204 | } 205 | 206 | choose_lokl_site_template() { 207 | lokl_log "Detecting Lokl site templates" 208 | LOKL_TEMPLATE_DIR="$HOME/.lokl/templates" 209 | 210 | # pseudo-code 211 | 212 | # if $HOME/.lokl/templates exists 213 | if [ -d "$LOKL_TEMPLATE_DIR" ]; then 214 | # and templates exist 215 | # shellcheck disable=SC2012,SC2086 216 | template_total="$(ls $LOKL_TEMPLATE_DIR/*.lokl | wc -l)" 217 | 218 | # collect valid template names 219 | if [ "$template_total" -gt 0 ]; then 220 | clear 221 | 222 | # iterate each template file 223 | OLDIFS="$IFS" 224 | IFS=' 225 | ' 226 | 227 | # shellcheck disable=SC2086 228 | TEMPLATE_FILES="$(ls $LOKL_TEMPLATE_DIR/*.lokl)" 229 | TEMPLATE_COUNTER=1 230 | 231 | echo "" 232 | echo "Lokl Site Templates Found" 233 | echo "" 234 | echo "Please choose one to use for this site:" 235 | echo "" 236 | 237 | for TEMPLATE_FILE in $TEMPLATE_FILES 238 | do 239 | # TODO: validate it contains required fields (VOLUMES) 240 | 241 | TEMPLATE_NAME="$(basename "$TEMPLATE_FILE" | cut -f 1 -d '.')" 242 | 243 | # print choices for user 244 | echo "$TEMPLATE_COUNTER) $TEMPLATE_NAME" 245 | lokl_log "$TEMPLATE_COUNTER) $TEMPLATE_NAME" 246 | 247 | TEMPLATE_COUNTER=$((TEMPLATE_COUNTER+1)) 248 | done 249 | IFS="$OLDIFS" 250 | 251 | echo "" 252 | echo "0) Don't use any template for this site" 253 | echo "" 254 | 255 | # wait for user to choose from list of template names 256 | read -r choose_site_template_choice 257 | 258 | CHOSEN_TEMPLATE_INDEX="$choose_site_template_choice" 259 | 260 | lokl_log "User chose site template #$CHOSEN_TEMPLATE_INDEX" 261 | 262 | if [ "$CHOSEN_TEMPLATE_INDEX" != "0" ]; then 263 | # do the for loop again, stopping when index matches 264 | # the chosen site index, then: 265 | OLDIFS="$IFS" 266 | IFS=' 267 | ' 268 | TEMPLATE_COUNTER=1 269 | for TEMPLATE_FILE in $TEMPLATE_FILES 270 | do 271 | if [ "$TEMPLATE_COUNTER" -eq "$CHOSEN_TEMPLATE_INDEX" ]; then 272 | # load template values 273 | lokl_log "Loading site template from $TEMPLATE_FILE" 274 | 275 | PARSE_VOLUMES="" 276 | 277 | # TODO: [[ isn't POSIX compatible 278 | # shellcheck disable=SC3010 279 | while IFS= read -r line || [[ -n "$line" ]]; do 280 | TRIMMED_LINE="$(echo "$line" | xargs)" 281 | 282 | lokl_log "Line from file: $TRIMMED_LINE" 283 | 284 | lokl_log "Parse volumes?: $PARSE_VOLUMES" 285 | 286 | # if line equals VOLUMES, concat subsequent non empty 287 | # lines to VOLUMES_TO_MOUNT var, pipe separated 288 | if [ "$PARSE_VOLUMES" = "1" ]; then 289 | if [ -n "$TRIMMED_LINE" ]; then 290 | lokl_log "Recording volume line: $TRIMMED_LINE" 291 | # delimiter in front to allow replaceing with -v 292 | VOLUMES_TO_MOUNT="|$TRIMMED_LINE$VOLUMES_TO_MOUNT" 293 | fi 294 | fi 295 | 296 | # TODO: optimize to not check once flag set 297 | if [ "$TRIMMED_LINE" = "VOLUMES" ]; then 298 | PARSE_VOLUMES="1" 299 | fi 300 | 301 | done < "$TEMPLATE_FILE" 302 | 303 | lokl_log "Concatenated volumes to mount:" 304 | lokl_log "$VOLUMES_TO_MOUNT" 305 | 306 | # set Lokl PHP version variable 307 | # set mount paths 308 | 309 | break 310 | fi 311 | 312 | TEMPLATE_COUNTER=$((TEMPLATE_COUNTER+1)) 313 | done 314 | IFS="$OLDIFS" 315 | fi 316 | else 317 | lokl_log "Lokl site template directory didn't contain templates" 318 | fi 319 | else 320 | lokl_log "No Lokl site templates directory found" 321 | fi 322 | 323 | create_site_choose_php_version 324 | } 325 | 326 | create_wordpress_docker_container() { 327 | LOKL_PORT="$(get_random_port)" 328 | 329 | lokl_log "Random port number generated: $LOKL_PORT" 330 | lokl_log "Using Docker tag: $LOKL_DOCKER_TAG" 331 | 332 | 333 | # shellcheck disable=SC2236 334 | if [ ! -z "$VOLUMES_TO_MOUNT" ]; then 335 | VOLUME_MOUNT_STRING="$(echo "$VOLUMES_TO_MOUNT" | sed 's/|/ -v /g')" 336 | 337 | # format volume mounting command if any set from template 338 | lokl_log "Running with mounted volumes: $VOLUME_MOUNT_STRING" 339 | 340 | # shellcheck disable=SC2086 341 | docker run -e N="$LOKL_NAME" -e P="$LOKL_PORT" \ 342 | --name="$LOKL_NAME" -p "$LOKL_PORT":"$LOKL_PORT" \ 343 | $VOLUME_MOUNT_STRING \ 344 | -d lokl/lokl:"$LOKL_DOCKER_TAG" 345 | else 346 | lokl_log "Running without any mounted volumes" 347 | 348 | docker run -e N="$LOKL_NAME" -e P="$LOKL_PORT" \ 349 | --name="$LOKL_NAME" -p "$LOKL_PORT":"$LOKL_PORT" \ 350 | -d lokl/lokl:"$LOKL_DOCKER_TAG" 351 | fi 352 | 353 | clear 354 | echo "Launching your new Lokl WordPress site!" 355 | echo "" 356 | 357 | wait_for_site_reachable "$LOKL_NAME" "$LOKL_PORT" 358 | 359 | if [ "$LOKL_NONINTERACTIVE_MODE" ]; then 360 | lokl_log "Site successfully launched non-interactively" 361 | exit 0 362 | fi 363 | 364 | 365 | clear 366 | echo "Your new Lokl WordPress site, $LOKL_NAME, is ready at:" 367 | echo "" 368 | echo "http://localhost:$LOKL_PORT" 369 | echo "" 370 | echo "Press any key to manage sites:" 371 | 372 | # return for assertion while testing 373 | if [ "$LOKL_TEST_MODE" ]; then 374 | lokl_log "Returning early for assertion under test runner" 375 | exit 0 376 | fi 377 | 378 | read -r "" 379 | manage_sites_menu 380 | } 381 | 382 | wait_for_site_reachable() { 383 | lokl_name="$1" 384 | lokl_port="$2" 385 | 386 | echo "Waiting for $lokl_name to be ready at http://localhost:$lokl_port" 387 | 388 | # poll until site accessible, print progresss 389 | attempt_counter=0 390 | max_attempts="$(set_curl_timeout_max_attempts "$LOKL_TEST_MODE")" 391 | site_poll_sleep_duration="$(set_site_poll_sleep_duration "$LOKL_TEST_MODE")" 392 | 393 | lokl_log "Waiting for: $max_attempts curl timeout attempts" 394 | 395 | until curl --output /dev/null --silent --head --fail "http://localhost:$lokl_port"; do 396 | 397 | if [ ${attempt_counter} -eq "${max_attempts}" ]; then 398 | echo "Timed out waiting for site to come online..." 399 | exit 1 400 | fi 401 | 402 | printf '.' 403 | attempt_counter=$((attempt_counter+1)) 404 | sleep "$site_poll_sleep_duration" 405 | done 406 | } 407 | 408 | manage_sites_menu() { 409 | # purposely put this after main_menu() to allow users to see what the wizard 410 | # is like before reporting any detected inabilities to create a site, which 411 | # also requires a little delay. ie, UX over logical function flow 412 | test_core_capabilities 413 | clear 414 | echo "" 415 | echo "Your Lokl WordPress sites" 416 | echo "" 417 | 418 | LOKL_CONTAINERS="$(get_lokl_container_ids)" 419 | 420 | # handle no container 421 | if [ -z "$LOKL_CONTAINERS" ]; then 422 | echo "" 423 | echo "No site created." 424 | echo "" 425 | echo "Press any key to create a new site or q to quit: " 426 | echo "" 427 | 428 | read -r create_site_choice 429 | 430 | if [ "$create_site_choice" != "q" ]; then 431 | create_site_choose_name 432 | return 0 433 | else 434 | exit 0 435 | fi 436 | fi 437 | 438 | generate_site_list 439 | 440 | echo "" 441 | echo "Choose the site you want to manage." 442 | echo "" 443 | echo "Type your site's number, then the Enter key: " 444 | echo "" 445 | 446 | read -r site_to_manage_choice 447 | 448 | # check int selected is in range of available sites 449 | if [ ! -f "/tmp/lokl_containers_cache/$site_to_manage_choice" ]; then 450 | echo "Requested site not found, try again" 451 | manage_sites_menu 452 | else 453 | manage_single_site 454 | fi 455 | } 456 | 457 | start_if_stopped() { 458 | if [ "$CONTAINER_STATE" != "running" ]; then 459 | clear 460 | echo "$CONTAINER_NAME was stopped, so we're re-launching it" 461 | echo "before performing your desired action..." 462 | echo "" 463 | 464 | docker start "$CONTAINER_ID" > /dev/null 465 | 466 | # need to get container port again here 467 | # get container's exposed port 468 | CONTAINER_PORT="$(docker inspect --format='{{.NetworkSettings.Ports}}' "$CONTAINER_ID" | \ 469 | sed 's/^[^{]*{\([^{}]*\)}.*/\1/' | awk '{print $2}')" 470 | 471 | wait_for_site_reachable "$CONTAINER_NAME" "$CONTAINER_PORT" 472 | fi 473 | } 474 | 475 | kill_container() { 476 | clear 477 | echo "Are you sure you want to force quit $CONTAINER_NAME?" 478 | echo "" 479 | echo "Type 'y' for yes:" 480 | 481 | read -r confirm_kill_container 482 | 483 | if [ "$confirm_kill_container" != "y" ]; then 484 | manage_single_site 485 | else 486 | echo "Stopping $CONTAINER_NAME's server." 487 | echo "" 488 | echo "Lokl will attempt to launch it again as you need it" 489 | echo "" 490 | docker kill "$CONTAINER_ID" > /dev/null 491 | fi 492 | } 493 | 494 | delete_container() { 495 | clear 496 | echo "Are you sure you want to delete $CONTAINER_NAME completely?" 497 | echo "" 498 | echo "Type 'y' for yes:" 499 | 500 | read -r confirm_delete_container 501 | 502 | if [ "$confirm_delete_container" != "y" ]; then 503 | manage_single_site 504 | else 505 | echo "Deleting $CONTAINER_NAME completely." 506 | echo "" 507 | docker rm "$CONTAINER_ID" > /dev/null 508 | fi 509 | } 510 | 511 | manage_single_site() { 512 | clear 513 | 514 | # load lokl container info from cache file 515 | CONTAINER_INFO=$(cat "/tmp/lokl_containers_cache/$site_to_manage_choice") 516 | CONTAINER_ID=$(echo "$CONTAINER_INFO" | cut -f1 -d,) 517 | CONTAINER_NAME=$(echo "$CONTAINER_INFO" | cut -f2 -d,) 518 | CONTAINER_PORT=$(echo "$CONTAINER_INFO" | cut -f3 -d,) 519 | CONTAINER_STATE=$(echo "$CONTAINER_INFO" | cut -f4 -d,) 520 | 521 | # print out details 522 | echo "Site: $CONTAINER_NAME" 523 | echo "Status: $CONTAINER_STATE" 524 | echo "" 525 | echo "Choose action to perform: " 526 | echo "" 527 | echo "o) open site http://localhost:$CONTAINER_PORT" 528 | echo "a) open WordPress admin /wp-admin" 529 | echo "p) open phpMyAdmin /phpmyadmin" 530 | echo "s) SSH into container" 531 | echo "t) take backup of site files and database" 532 | echo "l) follow server error logs" 533 | 534 | if [ "$CONTAINER_STATE" = "running" ]; then 535 | echo "k) kill (force quit) site's server" 536 | fi 537 | 538 | if [ "$CONTAINER_STATE" != "running" ]; then 539 | echo "d) delete server and site completely" 540 | fi 541 | 542 | echo "" 543 | echo "m) Back to manage sites menu" 544 | echo "q) Quit this menu" 545 | echo "" 546 | read -r site_action_choice 547 | 548 | if [ "$site_action_choice" != "${site_action_choice#[oapstlkdmq]}" ]; then 549 | case $site_action_choice in 550 | o|O) open_site_in_browser ;; 551 | a|A) open_wordpress_admin ;; 552 | p|P) open_phpmyadmin ;; 553 | s|S) ssh_into_container ;; 554 | t|T) take_site_backup ;; 555 | l|L) follow_error_logs ;; 556 | m|M) manage_sites_menu ;; 557 | k|K) kill_container ;; 558 | d|D) delete_container ;; 559 | q|Q) exit 0 ;; 560 | esac 561 | 562 | else 563 | manage_single_site 564 | fi 565 | } 566 | 567 | # take DB and files backup of site 568 | take_site_backup() { 569 | start_if_stopped 570 | clear 571 | echo "Generating backup file in container..." 572 | echo "" 573 | docker exec -it "$CONTAINER_ID" /backup_site.sh 574 | echo "Saving backup to host computer in path:" 575 | echo "" 576 | echo "/tmp/${CONTAINER_NAME}_SITE_BACKUP.tar.gz" 577 | echo "" 578 | docker cp "$CONTAINER_ID:/tmp/${CONTAINER_NAME}_SITE_BACKUP.tar.gz" \ 579 | "/tmp/${CONTAINER_NAME}_SITE_BACKUP.tar.gz" 580 | 581 | # ensure file was generated 582 | if [ ! -f "/tmp/${CONTAINER_NAME}_SITE_BACKUP.tar.gz" ]; then 583 | echo "Failed to save backup, try again" 584 | exit 1 585 | else 586 | echo "Backup complete" 587 | echo "" 588 | exit 0 589 | fi 590 | } 591 | 592 | # shell connect to container using Docker 593 | ssh_into_container() { 594 | start_if_stopped 595 | clear 596 | echo "Connecting to $CONTAINER_NAME via SSH" 597 | echo "" 598 | docker exec -it "$CONTAINER_ID" /bin/sh 599 | } 600 | 601 | follow_error_logs() { 602 | start_if_stopped 603 | clear 604 | echo "Following error logs for $CONTAINER_NAME:" 605 | echo "" 606 | docker logs -f "$CONTAINER_ID" 607 | } 608 | 609 | # open site in default browser 610 | open_site_in_browser() { 611 | start_if_stopped 612 | 613 | SITE_URL="http://localhost:$CONTAINER_PORT" 614 | 615 | if command -v xdg-open > /dev/null; then 616 | clear 617 | echo "Opening $SITE_URL in your browser." 618 | xdg-open "$SITE_URL" 619 | elif command -v gnome-open > /dev/null; then 620 | clear 621 | echo "Opening $SITE_URL in your browser." 622 | gnome-open "$SITE_URL" 623 | elif open -Ra "safari" ; then 624 | clear 625 | echo "Opening $SITE_URL in Safari." 626 | open -a safari "$SITE_URL" 627 | else 628 | echo "Couldn't detect the web browser to use." 629 | echo "" 630 | echo "Please manually open this URL in your browser:" 631 | echo "" 632 | echo "$SITE_URL" 633 | fi 634 | } 635 | 636 | open_wordpress_admin() { 637 | start_if_stopped 638 | 639 | SITE_URL="http://localhost:$CONTAINER_PORT/wp-admin/" 640 | 641 | if command -v xdg-open > /dev/null; then 642 | clear 643 | echo "Opening $SITE_URL in your browser." 644 | xdg-open "$SITE_URL" 645 | elif command -v gnome-open > /dev/null; then 646 | clear 647 | echo "Opening $SITE_URL in your browser." 648 | gnome-open "$SITE_URL" 649 | elif open -Ra "safari" ; then 650 | clear 651 | echo "Opening $SITE_URL in Safari." 652 | open -a safari "$SITE_URL" 653 | else 654 | echo "Couldn't detect the web browser to use." 655 | echo "" 656 | echo "Please manually open this URL in your browser:" 657 | echo "" 658 | echo "$SITE_URL" 659 | fi 660 | } 661 | 662 | open_phpmyadmin() { 663 | start_if_stopped 664 | 665 | SITE_URL="http://localhost:$CONTAINER_PORT/phpmyadmin/" 666 | 667 | if command -v xdg-open > /dev/null; then 668 | clear 669 | echo "Opening $SITE_URL in your browser." 670 | xdg-open "$SITE_URL" 671 | elif command -v gnome-open > /dev/null; then 672 | clear 673 | echo "Opening $SITE_URL in your browser." 674 | gnome-open "$SITE_URL" 675 | elif open -Ra "safari" ; then 676 | clear 677 | echo "Opening $SITE_URL in Safari." 678 | open -a safari "$SITE_URL" 679 | else 680 | echo "Couldn't detect the web browser to use." 681 | echo "" 682 | echo "Please manually open this URL in your browser:" 683 | echo "" 684 | echo "$SITE_URL" 685 | fi 686 | } 687 | 688 | get_random_port() { 689 | if [ "$LOKL_PORT" ]; then 690 | echo "$LOKL_PORT" 691 | return 0 692 | fi 693 | 694 | # echo value to stdout to be used in cmd substitution 695 | awk -v min=4000 -v max=5000 'BEGIN{srand(); print int(min+rand()*(max-min+1))}' 696 | 697 | # TODO: check for unused port to avoid collision 698 | } 699 | 700 | get_lokl_container_ids() { 701 | docker ps -a | awk '{ print $1,$2 }' | grep lokl | awk '{print $1 }' 702 | } 703 | 704 | generate_site_list() { 705 | # empty flatfile lokl containers cache 706 | rm -Rf /tmp/lokl_containers_cache/* 707 | mkdir -p /tmp/lokl_containers_cache/ 708 | 709 | SITE_COUNTER=1 710 | 711 | # POSIX compliant way to iterate a list 712 | OLDIFS="$IFS" 713 | IFS=' 714 | ' 715 | for CONTAINER_ID in $LOKL_CONTAINERS 716 | do 717 | CONTAINER_NAME="$(get_container_name_from_id "$CONTAINER_ID")" 718 | CONTAINER_PORT="$(get_container_port_from_id "$CONTAINER_ID")" 719 | CONTAINER_STATE="$(get_container_state_from_id "$CONTAINER_ID")" 720 | 721 | # print choices for user 722 | echo "$SITE_COUNTER) $CONTAINER_NAME" 723 | 724 | # append choices in cache file named for site counter (brittle internal ID) 725 | echo "$CONTAINER_ID,$CONTAINER_NAME,$CONTAINER_PORT,$CONTAINER_STATE" >> /tmp/lokl_containers_cache/$SITE_COUNTER 726 | 727 | SITE_COUNTER=$((SITE_COUNTER+1)) 728 | done 729 | IFS="$OLDIFS" 730 | } 731 | 732 | get_container_name_from_id() { 733 | docker inspect --format='{{.Name}}' "$1" | sed 's|/||' 734 | } 735 | 736 | get_container_port_from_id() { 737 | docker inspect --format='{{.NetworkSettings.Ports}}' "$1" | \ 738 | sed 's/^[^{]*{\([^{}]*\)}.*/\1/' | awk '{print $2}' 739 | } 740 | 741 | get_container_state_from_id() { 742 | docker inspect --format='{{.State.Status}}' "$1" 743 | } 744 | 745 | sanitize_site_name() { 746 | USER_SITE_NAME_CHOICE="$1" 747 | 748 | # strip all non-alpha characters from string, converts to lowercase 749 | # trims all hyphens 750 | # trim to 100 chars if over 751 | echo "$USER_SITE_NAME_CHOICE" | tr -cd '[:alnum:]-' | \ 752 | tr '[:upper:]' '[:lower:]' | sed 's/--//g' | sed 's/^-//' | sed 's/-$//' | \ 753 | cut -c1-100 754 | } 755 | 756 | # if running tests, export var to use as flag within functions 757 | # TODO: could put this back in spec_helper, but may annoy shellcheck 758 | if [ "${__SOURCED__}" ]; then 759 | lokl_log "### LOKL TEST MODE ENABLED ###" 760 | export LOKL_TEST_MODE=1 761 | fi 762 | 763 | LOKL_DOCKER_TAG="$(set_docker_tag)" 764 | LOKL_NAME="$(set_site_name)" 765 | LOKL_PORT="$(set_site_port)" 766 | LOKL_RELEASE_VERSION="5.0.0" 767 | VOLUMES_TO_MOUNT="" 768 | 769 | lokl_log "Using Docker tag: $LOKL_DOCKER_TAG" 770 | 771 | # allow testing without entering menu, using shellspec's var 772 | ${__SOURCED__:+return} 773 | 774 | # skip menu if minimum required arguments are set 775 | if [ "${LOKL_NAME}" ]; then 776 | export LOKL_NONINTERACTIVE_MODE=1 777 | lokl_log "Skipping wizard" 778 | lokl_log "Site Name Argument Passed: $LOKL_NAME" 779 | lokl_log "Site Port Argument Passed: $LOKL_PORT" 780 | lokl_log "Docker Tag Argument Passed: $LOKL_DOCKER_TAG" 781 | 782 | create_wordpress_docker_container 783 | else 784 | 785 | main_menu 786 | fi 787 | 788 | 789 | exit 0 790 | 791 | -------------------------------------------------------------------------------- /site-template-example.lokl: -------------------------------------------------------------------------------- 1 | # Lokl configuration file example 2 | # 3 | # Add to a $HOME/.lokl/templates/ directory 4 | # 5 | # This will allow you to choose to create a site from template 6 | # 7 | # Initially, allowing you to set volumes to mount 8 | # 9 | # Future versions may add extra functionality, like installing 10 | # extra plugins/themes, etc during provisioning 11 | # 12 | # Usage: 13 | # 14 | # Name the template file for recognition, ie default-php8-mount-my-plugins 15 | # 16 | # Template selection is offered after choosing a name for your site, 17 | # if your $HOME/.lokl/templates/ directory contains any valid templates 18 | # 19 | # Required template fields: 20 | # 21 | # VOLUMES 22 | # 23 | # In format SOURCE:DESTINATION 24 | # 25 | # I chose to risk the case of not working with volumes containing colons 26 | # in path in order to keep simple. If the bug is raised, I can look at 27 | # --mount instead. 28 | # 29 | # License: The Unlicense, https://unlicense.org 30 | 31 | VOLUMES 32 | 33 | /Users/leon/mypluginsrc:/usr/html/wp-content/plugins/myplugin 34 | /Users/leon/mythemesrc:/usr/html/wp-content/themes/mytheme 35 | -------------------------------------------------------------------------------- /spec/cli_spec.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh disable=SC2034 2 | Describe "cli.sh" 3 | Include ./cli.sh 4 | 5 | xIt "exits when under test" 6 | 7 | End 8 | 9 | xIt "skips wizard and " 10 | 11 | End 12 | 13 | Describe "lokl_log()" 14 | It "prints message to log file" 15 | 16 | # mock date '+%H:%M:%S' 17 | date() { 18 | echo '22:33:44' 19 | } 20 | 21 | # clear log before test 22 | echo '' > /tmp/lokldebuglog 23 | 24 | When call lokl_log 'My log message' 25 | The contents of file "/tmp/lokldebuglog" should equal " 26 | 22:33:44: My log message" 27 | The status should be success 28 | End 29 | End 30 | 31 | Describe "get_container_state_from_id()" 32 | It "returns container port" 33 | 34 | # mock docker inspect --format='{{.State.Status}}' 35 | docker() { 36 | echo 'running' 37 | } 38 | 39 | When call get_container_state_from_id 'someid' 40 | The output should equal 'running' 41 | The status should be success 42 | End 43 | End 44 | 45 | Describe "get_container_port_from_id()" 46 | It "returns container port" 47 | 48 | # mock docker inspect --format='{{.NetworkSettings.Ports}}' 49 | docker() { 50 | cat ./spec/test-data/docker-inspect-port-mapping-0-0-0-0-4363 51 | } 52 | 53 | When call get_container_port_from_id 'someid' 54 | The output should equal '4363' 55 | The status should be success 56 | End 57 | End 58 | 59 | Describe "get_container_name_from_id()" 60 | It "returns container name with leading slash stripped" 61 | 62 | # mock docker inspect --format='{{.Name}}' 63 | docker() { 64 | echo '/checktimeouts' 65 | } 66 | 67 | When call get_container_name_from_id 'someid' 68 | The output should equal 'checktimeouts' 69 | The status should be success 70 | End 71 | End 72 | 73 | Describe "set_site_poll_sleep_duration()" 74 | It "uses 5 seconds in production " 75 | When call set_site_poll_sleep_duration 0 76 | The output should equal '5' 77 | The status should be success 78 | End 79 | 80 | It "uses 0.1 seconds under test " 81 | When call set_site_poll_sleep_duration 1 82 | The output should equal '0.1' 83 | The status should be success 84 | End 85 | End 86 | 87 | Describe "set_curl_timeout_max_attempts()" 88 | It "tries 12 times in production " 89 | When call set_curl_timeout_max_attempts 0 90 | The output should equal '12' 91 | The status should be success 92 | End 93 | 94 | It "tries twice under test " 95 | When call set_curl_timeout_max_attempts 1 96 | The output should equal '2' 97 | The status should be success 98 | End 99 | End 100 | 101 | Describe "manage_sites_menu()" 102 | It "alerts that no site had been created and quit" 103 | docker() { 104 | echo "" 105 | } 106 | 107 | Data "q" 108 | When run manage_sites_menu 109 | The output should include 'No site created.' 110 | The status should be success 111 | End 112 | 113 | It "alerts that no site had been created and show creation menu" 114 | docker() { 115 | echo "" 116 | } 117 | 118 | Data "a" 119 | When run manage_sites_menu 120 | The output should include 'Type your site name' 121 | The status should equal 1 122 | End 123 | End 124 | 125 | Describe "generate_site_list()" 126 | It "prints numerically indexed list of site names" 127 | 128 | export LOKL_CONTAINERS="6f515f11c638 129 | 7874d8ccd920" 130 | 131 | get_container_name_from_id() { 132 | echo 'mywptestsite' 133 | } 134 | 135 | get_container_port_from_id() { 136 | echo '4455' 137 | } 138 | 139 | get_container_state_from_id() { 140 | echo 'running' 141 | } 142 | 143 | When call generate_site_list 144 | The line 1 should equal '1) mywptestsite' 145 | The line 2 should equal '2) mywptestsite' 146 | The status should be success 147 | End 148 | 149 | It "saves each site's container information to temp cache file" 150 | 151 | export LOKL_CONTAINERS="6f515f11c638 152 | 7874d8ccd920" 153 | 154 | get_container_name_from_id() { 155 | echo 'mywptestsite' 156 | } 157 | 158 | get_container_port_from_id() { 159 | echo '4455' 160 | } 161 | 162 | get_container_state_from_id() { 163 | echo 'running' 164 | } 165 | 166 | When call generate_site_list 167 | # without including these assertions, we get warning 168 | The line 1 should equal '1) mywptestsite' 169 | The line 2 should equal '2) mywptestsite' 170 | # TODO: isolate from real Lokl sites on test machine 171 | Path cache-file-1=/tmp/lokl_containers_cache/1 172 | Path cache-file-2=/tmp/lokl_containers_cache/2 173 | The path cache-file-1 should be exist 174 | The path cache-file-2 should be exist 175 | The status should be success 176 | End 177 | End 178 | 179 | Describe "get_random_port()" 180 | It "returns random port between 4000 and 5000" 181 | # shellcheck disable=SC2154 182 | acceptable_port_number() { 183 | [ "$acceptable_port_number" -ge 3999 ] && [ "$acceptable_port_number" -le 5001 ] 184 | } 185 | 186 | When call get_random_port 187 | The output should satisfy acceptable_port_number 188 | The status should be success 189 | End 190 | End 191 | 192 | Describe "get_lokl_container_ids()" 193 | It "returns only Lokl container IDs" 194 | 195 | # mock docker ps -a with lokl and non-lokl containers 196 | docker(){ 197 | cat ./spec/test-data/docker-ps-a-output-mixed-001 198 | } 199 | 200 | When call get_lokl_container_ids 201 | The output should equal "5577ddd81ed7 202 | 6ff99c2ca462 203 | f089aa00ac98 204 | 7874d8ccd920 205 | 9a54863ee7a2" 206 | The status should be success 207 | End 208 | End 209 | 210 | Describe "test_core_capabilities()" 211 | It "passes when docker and curl are available" 212 | 213 | test_docker_available(){ 214 | return 0 215 | } 216 | 217 | test_curl_available(){ 218 | return 0 219 | } 220 | 221 | When call test_core_capabilities 222 | The output should include "Checking system requirements..." 223 | The status should be success 224 | End 225 | 226 | It "fails when docker and curl aren't available" 227 | 228 | test_docker_available(){ 229 | return 1 230 | } 231 | 232 | test_curl_available(){ 233 | return 1 234 | } 235 | 236 | When call test_core_capabilities 237 | The output should include "Checking system requirements..." 238 | The status should be failure 239 | End 240 | End 241 | 242 | Describe "sanitize_site_name()" 243 | It "strips all non-alpha characters from string" 244 | When call sanitize_site_name "mywpte\$%@#\$@stsitename" 245 | The output should equal "mywptestsitename" 246 | The status should be success 247 | End 248 | 249 | It "converts chars to lowercase" 250 | When call sanitize_site_name "MYWPTEST SITE NAME" 251 | The output should equal "mywptestsitename" 252 | The status should be success 253 | End 254 | 255 | It "trims all hyphens" 256 | When call sanitize_site_name "-mywptest--sitename-" 257 | The output should equal "mywptestsitename" 258 | The status should be success 259 | End 260 | 261 | It "trims to 100 characters" 262 | When call sanitize_site_name "drbvkgdeqommcfsxrqfijlbzuayskgahltymfpckuexhykigdtoisuemtfqcabcdjdsfiipwkkowhspxjxwqkkecthisistheendoverflow" 263 | The output should equal "drbvkgdeqommcfsxrqfijlbzuayskgahltymfpckuexhykigdtoisuemtfqcabcdjdsfiipwkkowhspxjxwqkkecthisistheend" 264 | The status should be success 265 | End 266 | End 267 | 268 | Describe "main_menu()" 269 | It "exits when q is given" 270 | Data "q" 271 | When run main_menu 272 | The stdout should include 'Lokl launcher & management script' 273 | The status should be success 274 | End 275 | 276 | It "launches create_site_choose_name when c is given" 277 | create_site_choose_name() { 278 | echo "create_site_choose_name() called" 279 | } 280 | 281 | Data "c" 282 | When run main_menu 283 | The stdout should include 'create_site_choose_name() called' 284 | The status should be success 285 | End 286 | 287 | It "launches manage_sites_menu when c is given" 288 | manage_sites_menu() { 289 | echo "manage_sites_menu() called" 290 | } 291 | 292 | Data "m" 293 | When run main_menu 294 | The stdout should include 'manage_sites_menu() called' 295 | The status should be success 296 | End 297 | End 298 | 299 | Describe "set_site_port()" 300 | It "remains empty if $lokl_site_port not set" 301 | When call set_site_port 302 | The output should equal '' 303 | The status should be success 304 | End 305 | 306 | It "prints to stdout if $lokl_site_port set" 307 | lokl_site_port="4444" 308 | 309 | When call set_site_port 310 | The output should equal '4444' 311 | The status should be success 312 | End 313 | End 314 | 315 | Describe "set_site_name()" 316 | It "remains empty if $lokl_site_name not set" 317 | When call set_site_name 318 | The output should equal '' 319 | The status should be success 320 | End 321 | 322 | It "prints to stdout if $lokl_site_name set" 323 | lokl_site_name="mytestwpsitename" 324 | 325 | When call set_site_name 326 | The output should equal 'mytestwpsitename' 327 | The status should be success 328 | End 329 | End 330 | 331 | Describe "set_docker_tag()" 332 | It "defaults to php8-LOKL_RELEASE_VERSION if $lokl_php_ver not set" 333 | LOKL_RELEASE_VERSION=0.0.1 334 | 335 | When call set_docker_tag 336 | The output should equal 'php8-0.0.1' 337 | The status should be success 338 | End 339 | 340 | It "prints to stdout if $lokl_php_ver set" 341 | LOKL_RELEASE_VERSION=0.0.1 342 | lokl_php_ver="php7" 343 | 344 | When call set_docker_tag 345 | The output should equal 'php7-0.0.1' 346 | The status should be success 347 | End 348 | End 349 | 350 | Describe "test_curl_available()" 351 | It "returns OK when cURL is available" 352 | # simulate curl available 353 | command() { 354 | echo '/usr/bin/curl' 355 | return 0 356 | } 357 | 358 | When run test_curl_available 359 | The status should equal 0 360 | End 361 | 362 | It "exits with error code when cURL is missing" 363 | # simulate curl missing 364 | command() { 365 | return 1 366 | } 367 | 368 | When run test_curl_available 369 | The stdout should include "cURL doesn't seem to be installed." 370 | The status should equal 1 371 | End 372 | End 373 | 374 | Describe "test_docker_available()" 375 | It "returns OK when sample image runs OK" 376 | # simulate docker available and hello-world ran OK 377 | docker() { 378 | return 0 379 | } 380 | 381 | When run test_docker_available 382 | The status should equal 0 383 | End 384 | 385 | It "exits with error code when can't run sample image" 386 | # simulate docker unavailable or hello-world didn't run OK 387 | docker() { 388 | return 1 389 | } 390 | 391 | When run test_docker_available 392 | The stdout should include "Docker doesn't seem to be running or suitably configured for Lokl" 393 | The status should equal 1 394 | End 395 | End 396 | 397 | Describe "create_site_choose_php_version()" 398 | create_wordpress_docker_container() { 399 | echo "create_wordpress_docker_container() called" 400 | return 0 401 | } 402 | 403 | It "sets docker image tag to php8-LOKL_RELEASE_VERSION when 8 chosen" 404 | LOKL_RELEASE_VERSION=0.0.1 405 | Data "8" 406 | When call create_site_choose_php_version 407 | The variable LOKL_DOCKER_TAG should equal 'php8-0.0.1' 408 | The output should include \ 409 | 'create_wordpress_docker_container() called' 410 | The status should be success 411 | End 412 | 413 | It "sets docker image tag to php7-LOKL_RELEASE_VERSION when 7 chosen" 414 | LOKL_RELEASE_VERSION=0.0.1 415 | Data "7" 416 | When call create_site_choose_php_version 417 | The variable LOKL_DOCKER_TAG should equal 'php7-0.0.1' 418 | The output should include \ 419 | 'create_wordpress_docker_container() called' 420 | The status should be success 421 | End 422 | End 423 | 424 | Describe "create_site_choose_name()" 425 | It "sets LOKL_NAME to site name when proper name is given" 426 | # mock test_core_capabilities as not core to this test 427 | test_core_capabilities() { 428 | return 0 429 | } 430 | 431 | create_site_choose_php_version() { 432 | echo "create_site_choose_php_version() called" 433 | return 0 434 | } 435 | 436 | Data "mywptestsitename" 437 | When call create_site_choose_name 438 | The variable LOKL_NAME should equal 'mywptestsitename' 439 | The stdout should include \ 440 | 'create_site_choose_php_version() called' 441 | The status should be success 442 | End 443 | 444 | It "prompts for site name again if input invalid" 445 | test_core_capabilities() { 446 | return 0 447 | } 448 | 449 | Data "" 450 | When run create_site_choose_name 451 | The variable LOKL_NAME should equal "" 452 | The stdout should include 'Choose a name for your new Lokl WordPress site' 453 | The status should be failure 454 | End 455 | End 456 | 457 | Describe "create_wordpress_docker_container()" 458 | It "launches container with name and random port when proper name is given" 459 | LOKL_NAME="mywptestsitename" 460 | 461 | docker() { 462 | return 0 463 | } 464 | 465 | get_random_port() { 466 | echo "4070" 467 | } 468 | 469 | wait_for_site_reachable() { 470 | return 0 471 | } 472 | 473 | Data "mywptestsitename" 474 | When run create_wordpress_docker_container 475 | The stdout should include 'Your new Lokl WordPress site, mywptestsitename, is ready at:' 476 | The stdout should include 'http://localhost:4070' 477 | The status should be success 478 | End 479 | End 480 | 481 | Describe "wait_for_site_reachable()" 482 | It "exits if site doesn't come online after max polling duration" 483 | LOKL_TEST_MODE="1" 484 | 485 | curl() { 486 | echo "mocking unsuccessful curl to container..." 487 | return 1 488 | } 489 | 490 | When run wait_for_site_reachable "mywptestsitename" "4070" 491 | The lines of stdout should equal 5 492 | The stdout should include 'Timed out waiting for site to come online..' 493 | The status should be failure 494 | End 495 | 496 | It "succeeds if site is reachable within max polling duration" 497 | LOKL_TEST_MODE="1" 498 | 499 | curl() { 500 | return 0 501 | } 502 | 503 | When run wait_for_site_reachable "mywptestsitename" "4070" 504 | The lines of stdout should equal 1 505 | The stdout should equal \ 506 | 'Waiting for mywptestsitename to be ready at http://localhost:4070' 507 | The status should be success 508 | 509 | End 510 | End 511 | End 512 | 513 | 514 | -------------------------------------------------------------------------------- /spec/spec_helper.sh: -------------------------------------------------------------------------------- 1 | #shellcheck shell=sh 2 | # set -eu 3 | 4 | # shellspec_spec_helper_configure() { 5 | # shellspec_import 'support/custom_matcher' 6 | # } 7 | -------------------------------------------------------------------------------- /spec/test-data/docker-inspect-port-mapping-0-0-0-0-4363: -------------------------------------------------------------------------------- 1 | map[3465/tcp:[] 4000/tcp:[] 4001/tcp:[] 4002/tcp:[] 4003/tcp:[] 4004/tcp:[] 4005/tcp:[] 4006/tcp:[] 4007/tcp:[] 4008/tcp:[] 4009/tcp:[] 4010/tcp:[] 4011/tcp:[] 4012/tcp:[] 4013/tcp:[] 4014/tcp:[] 4015/tcp:[] 4016/tcp:[] 4017/tcp:[] 4018/tcp:[] 4019/tcp:[] 4020/tcp:[] 4021/tcp:[] 4022/tcp:[] 4023/tcp:[] 4024/tcp:[] 4025/tcp:[] 4026/tcp:[] 4027/tcp:[] 4028/tcp:[] 4029/tcp:[] 4030/tcp:[] 4031/tcp:[] 4032/tcp:[] 4033/tcp:[] 4034/tcp:[] 4035/tcp:[] 4036/tcp:[] 4037/tcp:[] 4038/tcp:[] 4039/tcp:[] 4040/tcp:[] 4041/tcp:[] 4042/tcp:[] 4043/tcp:[] 4044/tcp:[] 4045/tcp:[] 4046/tcp:[] 4047/tcp:[] 4048/tcp:[] 4049/tcp:[] 4050/tcp:[] 4051/tcp:[] 4052/tcp:[] 4053/tcp:[] 4054/tcp:[] 4055/tcp:[] 4056/tcp:[] 4057/tcp:[] 4058/tcp:[] 4059/tcp:[] 4060/tcp:[] 4061/tcp:[] 4062/tcp:[] 4063/tcp:[] 4064/tcp:[] 4065/tcp:[] 4066/tcp:[] 4067/tcp:[] 4068/tcp:[] 4069/tcp:[] 4070/tcp:[] 4071/tcp:[] 4072/tcp:[] 4073/tcp:[] 4074/tcp:[] 4075/tcp:[] 4076/tcp:[] 4077/tcp:[] 4078/tcp:[] 4079/tcp:[] 4080/tcp:[] 4081/tcp:[] 4082/tcp:[] 4083/tcp:[] 4084/tcp:[] 4085/tcp:[] 4086/tcp:[] 4087/tcp:[] 4088/tcp:[] 4089/tcp:[] 4090/tcp:[] 4091/tcp:[] 4092/tcp:[] 4093/tcp:[] 4094/tcp:[] 4095/tcp:[] 4096/tcp:[] 4097/tcp:[] 4098/tcp:[] 4099/tcp:[] 4100/tcp:[] 4101/tcp:[] 4102/tcp:[] 4103/tcp:[] 4104/tcp:[] 4105/tcp:[] 4106/tcp:[] 4107/tcp:[] 4108/tcp:[] 4109/tcp:[] 4110/tcp:[] 4111/tcp:[] 4112/tcp:[] 4113/tcp:[] 4114/tcp:[] 4115/tcp:[] 4116/tcp:[] 4117/tcp:[] 4118/tcp:[] 4119/tcp:[] 4120/tcp:[] 4121/tcp:[] 4122/tcp:[] 4123/tcp:[] 4124/tcp:[] 4125/tcp:[] 4126/tcp:[] 4127/tcp:[] 4128/tcp:[] 4129/tcp:[] 4130/tcp:[] 4131/tcp:[] 4132/tcp:[] 4133/tcp:[] 4134/tcp:[] 4135/tcp:[] 4136/tcp:[] 4137/tcp:[] 4138/tcp:[] 4139/tcp:[] 4140/tcp:[] 4141/tcp:[] 4142/tcp:[] 4143/tcp:[] 4144/tcp:[] 4145/tcp:[] 4146/tcp:[] 4147/tcp:[] 4148/tcp:[] 4149/tcp:[] 4150/tcp:[] 4151/tcp:[] 4152/tcp:[] 4153/tcp:[] 4154/tcp:[] 4155/tcp:[] 4156/tcp:[] 4157/tcp:[] 4158/tcp:[] 4159/tcp:[] 4160/tcp:[] 4161/tcp:[] 4162/tcp:[] 4163/tcp:[] 4164/tcp:[] 4165/tcp:[] 4166/tcp:[] 4167/tcp:[] 4168/tcp:[] 4169/tcp:[] 4170/tcp:[] 4171/tcp:[] 4172/tcp:[] 4173/tcp:[] 4174/tcp:[] 4175/tcp:[] 4176/tcp:[] 4177/tcp:[] 4178/tcp:[] 4179/tcp:[] 4180/tcp:[] 4181/tcp:[] 4182/tcp:[] 4183/tcp:[] 4184/tcp:[] 4185/tcp:[] 4186/tcp:[] 4187/tcp:[] 4188/tcp:[] 4189/tcp:[] 4190/tcp:[] 4191/tcp:[] 4192/tcp:[] 4193/tcp:[] 4194/tcp:[] 4195/tcp:[] 4196/tcp:[] 4197/tcp:[] 4198/tcp:[] 4199/tcp:[] 4200/tcp:[] 4201/tcp:[] 4202/tcp:[] 4203/tcp:[] 4204/tcp:[] 4205/tcp:[] 4206/tcp:[] 4207/tcp:[] 4208/tcp:[] 4209/tcp:[] 4210/tcp:[] 4211/tcp:[] 4212/tcp:[] 4213/tcp:[] 4214/tcp:[] 4215/tcp:[] 4216/tcp:[] 4217/tcp:[] 4218/tcp:[] 4219/tcp:[] 4220/tcp:[] 4221/tcp:[] 4222/tcp:[] 4223/tcp:[] 4224/tcp:[] 4225/tcp:[] 4226/tcp:[] 4227/tcp:[] 4228/tcp:[] 4229/tcp:[] 4230/tcp:[] 4231/tcp:[] 4232/tcp:[] 4233/tcp:[] 4234/tcp:[] 4235/tcp:[] 4236/tcp:[] 4237/tcp:[] 4238/tcp:[] 4239/tcp:[] 4240/tcp:[] 4241/tcp:[] 4242/tcp:[] 4243/tcp:[] 4244/tcp:[] 4245/tcp:[] 4246/tcp:[] 4247/tcp:[] 4248/tcp:[] 4249/tcp:[] 4250/tcp:[] 4251/tcp:[] 4252/tcp:[] 4253/tcp:[] 4254/tcp:[] 4255/tcp:[] 4256/tcp:[] 4257/tcp:[] 4258/tcp:[] 4259/tcp:[] 4260/tcp:[] 4261/tcp:[] 4262/tcp:[] 4263/tcp:[] 4264/tcp:[] 4265/tcp:[] 4266/tcp:[] 4267/tcp:[] 4268/tcp:[] 4269/tcp:[] 4270/tcp:[] 4271/tcp:[] 4272/tcp:[] 4273/tcp:[] 4274/tcp:[] 4275/tcp:[] 4276/tcp:[] 4277/tcp:[] 4278/tcp:[] 4279/tcp:[] 4280/tcp:[] 4281/tcp:[] 4282/tcp:[] 4283/tcp:[] 4284/tcp:[] 4285/tcp:[] 4286/tcp:[] 4287/tcp:[] 4288/tcp:[] 4289/tcp:[] 4290/tcp:[] 4291/tcp:[] 4292/tcp:[] 4293/tcp:[] 4294/tcp:[] 4295/tcp:[] 4296/tcp:[] 4297/tcp:[] 4298/tcp:[] 4299/tcp:[] 4300/tcp:[] 4301/tcp:[] 4302/tcp:[] 4303/tcp:[] 4304/tcp:[] 4305/tcp:[] 4306/tcp:[] 4307/tcp:[] 4308/tcp:[] 4309/tcp:[] 4310/tcp:[] 4311/tcp:[] 4312/tcp:[] 4313/tcp:[] 4314/tcp:[] 4315/tcp:[] 4316/tcp:[] 4317/tcp:[] 4318/tcp:[] 4319/tcp:[] 4320/tcp:[] 4321/tcp:[] 4322/tcp:[] 4323/tcp:[] 4324/tcp:[] 4325/tcp:[] 4326/tcp:[] 4327/tcp:[] 4328/tcp:[] 4329/tcp:[] 4330/tcp:[] 4331/tcp:[] 4332/tcp:[] 4333/tcp:[] 4334/tcp:[] 4335/tcp:[] 4336/tcp:[] 4337/tcp:[] 4338/tcp:[] 4339/tcp:[] 4340/tcp:[] 4341/tcp:[] 4342/tcp:[] 4343/tcp:[] 4344/tcp:[] 4345/tcp:[] 4346/tcp:[] 4347/tcp:[] 4348/tcp:[] 4349/tcp:[] 4350/tcp:[] 4351/tcp:[] 4352/tcp:[] 4353/tcp:[] 4354/tcp:[] 4355/tcp:[] 4356/tcp:[] 4357/tcp:[] 4358/tcp:[] 4359/tcp:[] 4360/tcp:[] 4361/tcp:[] 4362/tcp:[] 4363/tcp:[{0.0.0.0 4363}] 4364/tcp:[] 4365/tcp:[] 4366/tcp:[] 4367/tcp:[] 4368/tcp:[] 4369/tcp:[] 4370/tcp:[] 4371/tcp:[] 4372/tcp:[] 4373/tcp:[] 4374/tcp:[] 4375/tcp:[] 4376/tcp:[] 4377/tcp:[] 4378/tcp:[] 4379/tcp:[] 4380/tcp:[] 4381/tcp:[] 4382/tcp:[] 4383/tcp:[] 4384/tcp:[] 4385/tcp:[] 4386/tcp:[] 4387/tcp:[] 4388/tcp:[] 4389/tcp:[] 4390/tcp:[] 4391/tcp:[] 4392/tcp:[] 4393/tcp:[] 4394/tcp:[] 4395/tcp:[] 4396/tcp:[] 4397/tcp:[] 4398/tcp:[] 4399/tcp:[] 4400/tcp:[] 4401/tcp:[] 4402/tcp:[] 4403/tcp:[] 4404/tcp:[] 4405/tcp:[] 4406/tcp:[] 4407/tcp:[] 4408/tcp:[] 4409/tcp:[] 4410/tcp:[] 4411/tcp:[] 4412/tcp:[] 4413/tcp:[] 4414/tcp:[] 4415/tcp:[] 4416/tcp:[] 4417/tcp:[] 4418/tcp:[] 4419/tcp:[] 4420/tcp:[] 4421/tcp:[] 4422/tcp:[] 4423/tcp:[] 4424/tcp:[] 4425/tcp:[] 4426/tcp:[] 4427/tcp:[] 4428/tcp:[] 4429/tcp:[] 4430/tcp:[] 4431/tcp:[] 4432/tcp:[] 4433/tcp:[] 4434/tcp:[] 4435/tcp:[] 4436/tcp:[] 4437/tcp:[] 4438/tcp:[] 4439/tcp:[] 4440/tcp:[] 4441/tcp:[] 4442/tcp:[] 4443/tcp:[] 4444/tcp:[] 4445/tcp:[] 4446/tcp:[] 4447/tcp:[] 4448/tcp:[] 4449/tcp:[] 4450/tcp:[] 4451/tcp:[] 4452/tcp:[] 4453/tcp:[] 4454/tcp:[] 4455/tcp:[] 4456/tcp:[] 4457/tcp:[] 4458/tcp:[] 4459/tcp:[] 4460/tcp:[] 4461/tcp:[] 4462/tcp:[] 4463/tcp:[] 4464/tcp:[] 4465/tcp:[] 4466/tcp:[] 4467/tcp:[] 4468/tcp:[] 4469/tcp:[] 4470/tcp:[] 4471/tcp:[] 4472/tcp:[] 4473/tcp:[] 4474/tcp:[] 4475/tcp:[] 4476/tcp:[] 4477/tcp:[] 4478/tcp:[] 4479/tcp:[] 4480/tcp:[] 4481/tcp:[] 4482/tcp:[] 4483/tcp:[] 4484/tcp:[] 4485/tcp:[] 4486/tcp:[] 4487/tcp:[] 4488/tcp:[] 4489/tcp:[] 4490/tcp:[] 4491/tcp:[] 4492/tcp:[] 4493/tcp:[] 4494/tcp:[] 4495/tcp:[] 4496/tcp:[] 4497/tcp:[] 4498/tcp:[] 4499/tcp:[] 4500/tcp:[] 4501/tcp:[] 4502/tcp:[] 4503/tcp:[] 4504/tcp:[] 4505/tcp:[] 4506/tcp:[] 4507/tcp:[] 4508/tcp:[] 4509/tcp:[] 4510/tcp:[] 4511/tcp:[] 4512/tcp:[] 4513/tcp:[] 4514/tcp:[] 4515/tcp:[] 4516/tcp:[] 4517/tcp:[] 4518/tcp:[] 4519/tcp:[] 4520/tcp:[] 4521/tcp:[] 4522/tcp:[] 4523/tcp:[] 4524/tcp:[] 4525/tcp:[] 4526/tcp:[] 4527/tcp:[] 4528/tcp:[] 4529/tcp:[] 4530/tcp:[] 4531/tcp:[] 4532/tcp:[] 4533/tcp:[] 4534/tcp:[] 4535/tcp:[] 4536/tcp:[] 4537/tcp:[] 4538/tcp:[] 4539/tcp:[] 4540/tcp:[] 4541/tcp:[] 4542/tcp:[] 4543/tcp:[] 4544/tcp:[] 4545/tcp:[] 4546/tcp:[] 4547/tcp:[] 4548/tcp:[] 4549/tcp:[] 4550/tcp:[] 4551/tcp:[] 4552/tcp:[] 4553/tcp:[] 4554/tcp:[] 4555/tcp:[] 4556/tcp:[] 4557/tcp:[] 4558/tcp:[] 4559/tcp:[] 4560/tcp:[] 4561/tcp:[] 4562/tcp:[] 4563/tcp:[] 4564/tcp:[] 4565/tcp:[] 4566/tcp:[] 4567/tcp:[] 4568/tcp:[] 4569/tcp:[] 4570/tcp:[] 4571/tcp:[] 4572/tcp:[] 4573/tcp:[] 4574/tcp:[] 4575/tcp:[] 4576/tcp:[] 4577/tcp:[] 4578/tcp:[] 4579/tcp:[] 4580/tcp:[] 4581/tcp:[] 4582/tcp:[] 4583/tcp:[] 4584/tcp:[] 4585/tcp:[] 4586/tcp:[] 4587/tcp:[] 4588/tcp:[] 4589/tcp:[] 4590/tcp:[] 4591/tcp:[] 4592/tcp:[] 4593/tcp:[] 4594/tcp:[] 4595/tcp:[] 4596/tcp:[] 4597/tcp:[] 4598/tcp:[] 4599/tcp:[] 4600/tcp:[] 4601/tcp:[] 4602/tcp:[] 4603/tcp:[] 4604/tcp:[] 4605/tcp:[] 4606/tcp:[] 4607/tcp:[] 4608/tcp:[] 4609/tcp:[] 4610/tcp:[] 4611/tcp:[] 4612/tcp:[] 4613/tcp:[] 4614/tcp:[] 4615/tcp:[] 4616/tcp:[] 4617/tcp:[] 4618/tcp:[] 4619/tcp:[] 4620/tcp:[] 4621/tcp:[] 4622/tcp:[] 4623/tcp:[] 4624/tcp:[] 4625/tcp:[] 4626/tcp:[] 4627/tcp:[] 4628/tcp:[] 4629/tcp:[] 4630/tcp:[] 4631/tcp:[] 4632/tcp:[] 4633/tcp:[] 4634/tcp:[] 4635/tcp:[] 4636/tcp:[] 4637/tcp:[] 4638/tcp:[] 4639/tcp:[] 4640/tcp:[] 4641/tcp:[] 4642/tcp:[] 4643/tcp:[] 4644/tcp:[] 4645/tcp:[] 4646/tcp:[] 4647/tcp:[] 4648/tcp:[] 4649/tcp:[] 4650/tcp:[] 4651/tcp:[] 4652/tcp:[] 4653/tcp:[] 4654/tcp:[] 4655/tcp:[] 4656/tcp:[] 4657/tcp:[] 4658/tcp:[] 4659/tcp:[] 4660/tcp:[] 4661/tcp:[] 4662/tcp:[] 4663/tcp:[] 4664/tcp:[] 4665/tcp:[] 4666/tcp:[] 4667/tcp:[] 4668/tcp:[] 4669/tcp:[] 4670/tcp:[] 4671/tcp:[] 4672/tcp:[] 4673/tcp:[] 4674/tcp:[] 4675/tcp:[] 4676/tcp:[] 4677/tcp:[] 4678/tcp:[] 4679/tcp:[] 4680/tcp:[] 4681/tcp:[] 4682/tcp:[] 4683/tcp:[] 4684/tcp:[] 4685/tcp:[] 4686/tcp:[] 4687/tcp:[] 4688/tcp:[] 4689/tcp:[] 4690/tcp:[] 4691/tcp:[] 4692/tcp:[] 4693/tcp:[] 4694/tcp:[] 4695/tcp:[] 4696/tcp:[] 4697/tcp:[] 4698/tcp:[] 4699/tcp:[] 4700/tcp:[] 4701/tcp:[] 4702/tcp:[] 4703/tcp:[] 4704/tcp:[] 4705/tcp:[] 4706/tcp:[] 4707/tcp:[] 4708/tcp:[] 4709/tcp:[] 4710/tcp:[] 4711/tcp:[] 4712/tcp:[] 4713/tcp:[] 4714/tcp:[] 4715/tcp:[] 4716/tcp:[] 4717/tcp:[] 4718/tcp:[] 4719/tcp:[] 4720/tcp:[] 4721/tcp:[] 4722/tcp:[] 4723/tcp:[] 4724/tcp:[] 4725/tcp:[] 4726/tcp:[] 4727/tcp:[] 4728/tcp:[] 4729/tcp:[] 4730/tcp:[] 4731/tcp:[] 4732/tcp:[] 4733/tcp:[] 4734/tcp:[] 4735/tcp:[] 4736/tcp:[] 4737/tcp:[] 4738/tcp:[] 4739/tcp:[] 4740/tcp:[] 4741/tcp:[] 4742/tcp:[] 4743/tcp:[] 4744/tcp:[] 4745/tcp:[] 4746/tcp:[] 4747/tcp:[] 4748/tcp:[] 4749/tcp:[] 4750/tcp:[] 4751/tcp:[] 4752/tcp:[] 4753/tcp:[] 4754/tcp:[] 4755/tcp:[] 4756/tcp:[] 4757/tcp:[] 4758/tcp:[] 4759/tcp:[] 4760/tcp:[] 4761/tcp:[] 4762/tcp:[] 4763/tcp:[] 4764/tcp:[] 4765/tcp:[] 4766/tcp:[] 4767/tcp:[] 4768/tcp:[] 4769/tcp:[] 4770/tcp:[] 4771/tcp:[] 4772/tcp:[] 4773/tcp:[] 4774/tcp:[] 4775/tcp:[] 4776/tcp:[] 4777/tcp:[] 4778/tcp:[] 4779/tcp:[] 4780/tcp:[] 4781/tcp:[] 4782/tcp:[] 4783/tcp:[] 4784/tcp:[] 4785/tcp:[] 4786/tcp:[] 4787/tcp:[] 4788/tcp:[] 4789/tcp:[] 4790/tcp:[] 4791/tcp:[] 4792/tcp:[] 4793/tcp:[] 4794/tcp:[] 4795/tcp:[] 4796/tcp:[] 4797/tcp:[] 4798/tcp:[] 4799/tcp:[] 4800/tcp:[] 4801/tcp:[] 4802/tcp:[] 4803/tcp:[] 4804/tcp:[] 4805/tcp:[] 4806/tcp:[] 4807/tcp:[] 4808/tcp:[] 4809/tcp:[] 4810/tcp:[] 4811/tcp:[] 4812/tcp:[] 4813/tcp:[] 4814/tcp:[] 4815/tcp:[] 4816/tcp:[] 4817/tcp:[] 4818/tcp:[] 4819/tcp:[] 4820/tcp:[] 4821/tcp:[] 4822/tcp:[] 4823/tcp:[] 4824/tcp:[] 4825/tcp:[] 4826/tcp:[] 4827/tcp:[] 4828/tcp:[] 4829/tcp:[] 4830/tcp:[] 4831/tcp:[] 4832/tcp:[] 4833/tcp:[] 4834/tcp:[] 4835/tcp:[] 4836/tcp:[] 4837/tcp:[] 4838/tcp:[] 4839/tcp:[] 4840/tcp:[] 4841/tcp:[] 4842/tcp:[] 4843/tcp:[] 4844/tcp:[] 4845/tcp:[] 4846/tcp:[] 4847/tcp:[] 4848/tcp:[] 4849/tcp:[] 4850/tcp:[] 4851/tcp:[] 4852/tcp:[] 4853/tcp:[] 4854/tcp:[] 4855/tcp:[] 4856/tcp:[] 4857/tcp:[] 4858/tcp:[] 4859/tcp:[] 4860/tcp:[] 4861/tcp:[] 4862/tcp:[] 4863/tcp:[] 4864/tcp:[] 4865/tcp:[] 4866/tcp:[] 4867/tcp:[] 4868/tcp:[] 4869/tcp:[] 4870/tcp:[] 4871/tcp:[] 4872/tcp:[] 4873/tcp:[] 4874/tcp:[] 4875/tcp:[] 4876/tcp:[] 4877/tcp:[] 4878/tcp:[] 4879/tcp:[] 4880/tcp:[] 4881/tcp:[] 4882/tcp:[] 4883/tcp:[] 4884/tcp:[] 4885/tcp:[] 4886/tcp:[] 4887/tcp:[] 4888/tcp:[] 4889/tcp:[] 4890/tcp:[] 4891/tcp:[] 4892/tcp:[] 4893/tcp:[] 4894/tcp:[] 4895/tcp:[] 4896/tcp:[] 4897/tcp:[] 4898/tcp:[] 4899/tcp:[] 4900/tcp:[] 4901/tcp:[] 4902/tcp:[] 4903/tcp:[] 4904/tcp:[] 4905/tcp:[] 4906/tcp:[] 4907/tcp:[] 4908/tcp:[] 4909/tcp:[] 4910/tcp:[] 4911/tcp:[] 4912/tcp:[] 4913/tcp:[] 4914/tcp:[] 4915/tcp:[] 4916/tcp:[] 4917/tcp:[] 4918/tcp:[] 4919/tcp:[] 4920/tcp:[] 4921/tcp:[] 4922/tcp:[] 4923/tcp:[] 4924/tcp:[] 4925/tcp:[] 4926/tcp:[] 4927/tcp:[] 4928/tcp:[] 4929/tcp:[] 4930/tcp:[] 4931/tcp:[] 4932/tcp:[] 4933/tcp:[] 4934/tcp:[] 4935/tcp:[] 4936/tcp:[] 4937/tcp:[] 4938/tcp:[] 4939/tcp:[] 4940/tcp:[] 4941/tcp:[] 4942/tcp:[] 4943/tcp:[] 4944/tcp:[] 4945/tcp:[] 4946/tcp:[] 4947/tcp:[] 4948/tcp:[] 4949/tcp:[] 4950/tcp:[] 4951/tcp:[] 4952/tcp:[] 4953/tcp:[] 4954/tcp:[] 4955/tcp:[] 4956/tcp:[] 4957/tcp:[] 4958/tcp:[] 4959/tcp:[] 4960/tcp:[] 4961/tcp:[] 4962/tcp:[] 4963/tcp:[] 4964/tcp:[] 4965/tcp:[] 4966/tcp:[] 4967/tcp:[] 4968/tcp:[] 4969/tcp:[] 4970/tcp:[] 4971/tcp:[] 4972/tcp:[] 4973/tcp:[] 4974/tcp:[] 4975/tcp:[] 4976/tcp:[] 4977/tcp:[] 4978/tcp:[] 4979/tcp:[] 4980/tcp:[] 4981/tcp:[] 4982/tcp:[] 4983/tcp:[] 4984/tcp:[] 4985/tcp:[] 4986/tcp:[] 4987/tcp:[] 4988/tcp:[] 4989/tcp:[] 4990/tcp:[] 4991/tcp:[] 4992/tcp:[] 4993/tcp:[] 4994/tcp:[] 4995/tcp:[] 4996/tcp:[] 4997/tcp:[] 4998/tcp:[] 4999/tcp:[] 5000/tcp:[]] 2 | -------------------------------------------------------------------------------- /spec/test-data/docker-ps-a-output-mixed-001: -------------------------------------------------------------------------------- 1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2 | 5577ddd81ed7 lokl/lokl:php8 "/run.sh" 2 days ago Exited (255) 2 days ago 3465/tcp, 4000-4240/tcp, 4242-5000/tcp, 0.0.0.0:4241->4241/tcp mywptestsitename 3 | 877887f62d48 hello-world "/hello" 11 days ago Created fervent_buck 4 | 6ff99c2ca462 lokl/lokl:php8 "/run.sh" 2 weeks ago Exited (255) 3 days ago 3465/tcp, 4000-4046/tcp, 4048-5000/tcp, 0.0.0.0:4047->4047/tcp areal8site 5 | 220e9e723627 hello-world "/hello" 2 weeks ago Exited (0) 2 weeks ago relaxed_dewdney 6 | f089aa00ac98 lokl/lokl:php7 "/run.sh" 2 weeks ago Exited (255) 3 days ago 3466/tcp, 4000-4909/tcp, 4911-5000/tcp, 0.0.0.0:4910->4910/tcp anew8site 7 | 0560af546278 hello-world "/hello" 2 weeks ago Exited (0) 2 weeks ago compassionate_cannon 8 | 0f99bab56522 hello-world "/hello" 2 weeks ago Exited (0) 2 weeks ago nice_satoshi 9 | 11f7edc8a67b hello-world "/hello" 2 weeks ago Exited (0) 2 weeks ago serene_bassi 10 | 7874d8ccd920 lokl/lokl:php7 "/run.sh" 2 weeks ago Exited (255) 3 days ago 3466/tcp, 4000-4488/tcp, 4490-5000/tcp, 0.0.0.0:4489->4489/tcp moonday 11 | f919e951b48e hello-world "/hello" 2 weeks ago Exited (0) 2 weeks ago distracted_wilson 12 | 9a54863ee7a2 lokl/lokl:php7base "/run.sh" 2 weeks ago Exited (255) 3 days ago 0.0.0.0:3466->3466/tcp, 4000-5000/tcp php7base 13 | 22e43cc41c3b hello-world "/hello" 2 weeks ago Exited (0) 2 weeks ago priceless_payne 14 | 82420cd84c17 d005ed73eb8b "/run.sh" 2 weeks ago Exited (255) 3 days ago 3466/tcp, 4000-4340/tcp, 4342-5000/tcp, 0.0.0.0:4341->4341/tcp new7test 15 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # lokl-cli: shell script checker 4 | # 5 | # License: The Unlicense, https://unlicense.org 6 | # 7 | # Usage: execute this script from the project root 8 | # 9 | # $ sh tests.sh 10 | # 11 | 12 | # run ShellCheck to catch syntactical errors and promote best practice 13 | find . -type f -not -path '*/\.git/*' -name '*.sh' \ 14 | -exec shellcheck {} \; 15 | 16 | # run ShellSpec unit/integration tests and generate coverage report 17 | shellspec --kcov 18 | --------------------------------------------------------------------------------