From db8639fcfd00ebb2c37a5c0a6acb9d17baa232bf Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Sat, 12 Sep 2026 13:46:56 +0200 Subject: [PATCH 1/6] Generated exercice files with `configlet create` --- config.json | 8 ++++ .../save-the-cow/.docs/instructions.md | 7 ++++ .../save-the-cow/.docs/introduction.md | 4 ++ .../practice/save-the-cow/.meta/config.json | 15 +++++++ .../practice/save-the-cow/.meta/example.sh | 0 .../practice/save-the-cow/.meta/tests.toml | 40 +++++++++++++++++++ .../practice/save-the-cow/save_the_cow.bats | 0 .../practice/save-the-cow/save_the_cow.sh | 0 8 files changed, 74 insertions(+) create mode 100644 exercises/practice/save-the-cow/.docs/instructions.md create mode 100644 exercises/practice/save-the-cow/.docs/introduction.md create mode 100644 exercises/practice/save-the-cow/.meta/config.json create mode 100644 exercises/practice/save-the-cow/.meta/example.sh create mode 100644 exercises/practice/save-the-cow/.meta/tests.toml create mode 100644 exercises/practice/save-the-cow/save_the_cow.bats create mode 100644 exercises/practice/save-the-cow/save_the_cow.sh diff --git a/config.json b/config.json index d39f93ea..aa4b57ba 100644 --- a/config.json +++ b/config.json @@ -1244,6 +1244,14 @@ "associative_arrays", "control_flow_conditionals" ] + }, + { + "slug": "save-the-cow", + "name": "Save the Cow", + "uuid": "21b2a344-925e-427a-a2bb-69122df2ce61", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ], "foregone": [ diff --git a/exercises/practice/save-the-cow/.docs/instructions.md b/exercises/practice/save-the-cow/.docs/instructions.md new file mode 100644 index 00000000..1ec98a43 --- /dev/null +++ b/exercises/practice/save-the-cow/.docs/instructions.md @@ -0,0 +1,7 @@ +# Instructions + +Implement the logic for a word-guessing game. + +A player tries to solve a secret word by guessing individual letters. +They win if they reveal all the letters in the secret word. +They lose if they make ten incorrect guesses before revealing the word. diff --git a/exercises/practice/save-the-cow/.docs/introduction.md b/exercises/practice/save-the-cow/.docs/introduction.md new file mode 100644 index 00000000..c87a7fa9 --- /dev/null +++ b/exercises/practice/save-the-cow/.docs/introduction.md @@ -0,0 +1,4 @@ +# Introduction + +Bessie the cow has wandered onto an alien spaceship. +Guess the secret door code to bring her home before the ship blasts off. diff --git a/exercises/practice/save-the-cow/.meta/config.json b/exercises/practice/save-the-cow/.meta/config.json new file mode 100644 index 00000000..4d7a536c --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/config.json @@ -0,0 +1,15 @@ +{ + "authors": [], + "files": { + "solution": [ + "save_the_cow.sh" + ], + "test": [ + "save_the_cow.bats" + ], + "example": [ + ".meta/example.sh" + ] + }, + "blurb": "Implement a word-guessing game." +} diff --git a/exercises/practice/save-the-cow/.meta/example.sh b/exercises/practice/save-the-cow/.meta/example.sh new file mode 100644 index 00000000..e69de29b diff --git a/exercises/practice/save-the-cow/.meta/tests.toml b/exercises/practice/save-the-cow/.meta/tests.toml new file mode 100644 index 00000000..eca72c42 --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/tests.toml @@ -0,0 +1,40 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[71d340f9-fc29-4826-872e-ad7d0b83dd98] +description = "Initially 9 failures are allowed and no letters are guessed" + +[76759c24-8f1a-4fc8-9ffd-d6a1ff0cf03b] +description = "After 10 failures the game is over" + +[d6f2e202-7857-46fb-b709-43a7f3f3b2de] +description = "Losing with several correct guesses" + +[71bc0cda-2032-4637-80c8-fc8771124c08] +description = "Feeding a correct letter removes underscores" + +[5b568a1c-867d-418f-97a8-7b6f8a7ca0a2] +description = "Feeding a correct letter twice counts as a failure" + +[3d40f15b-0271-4c5d-b1a4-3e1f66ff221f] +description = "Guessing a repeated letter reveals all instances" + +[11a86435-e401-4250-a26e-3b0d8c4049ad] +description = "Getting all the letters right makes for a win" + +[b3d81876-84ee-45bb-b531-baa1b05b4709] +description = "Winning on the last guess is still a win" + +[cf204398-5e9f-402f-8bff-42cb7cbbbda9] +description = "Guessing after a lose is error" + +[c2ec5b3d-4923-4a0e-a485-6aa6e78c7ece] +description = "Guessing after a win is error" diff --git a/exercises/practice/save-the-cow/save_the_cow.bats b/exercises/practice/save-the-cow/save_the_cow.bats new file mode 100644 index 00000000..e69de29b diff --git a/exercises/practice/save-the-cow/save_the_cow.sh b/exercises/practice/save-the-cow/save_the_cow.sh new file mode 100644 index 00000000..e69de29b From 803e30c9ca63277238b4ead0bf83fae616b0e3ff Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Sat, 12 Sep 2026 22:43:22 +0200 Subject: [PATCH 2/6] Created `template.j2`, `.bats` file and `example.sh` --- .../practice/save-the-cow/.meta/example.sh | 41 ++ .../practice/save-the-cow/.meta/template.j2 | 15 + .../practice/save-the-cow/bats-extra.bash | 642 ++++++++++++++++++ .../practice/save-the-cow/save_the_cow.bats | 91 +++ .../practice/save-the-cow/save_the_cow.sh | 24 + 5 files changed, 813 insertions(+) create mode 100644 exercises/practice/save-the-cow/.meta/template.j2 create mode 100644 exercises/practice/save-the-cow/bats-extra.bash diff --git a/exercises/practice/save-the-cow/.meta/example.sh b/exercises/practice/save-the-cow/.meta/example.sh index e69de29b..e718f49c 100644 --- a/exercises/practice/save-the-cow/.meta/example.sh +++ b/exercises/practice/save-the-cow/.meta/example.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +guess() { + local word="$1"; shift + local -a guesses=( "$@" ) + local state="Ongoing" + local remaining_failures=9 + local masked_word=${word//?/_} + local i + + for guess in "${guesses[@]}"; do + if [[ $state == "Win" ]]; then + echo "cannot guess after the game is won" + elif [[ $state == "Lose" ]]; then + echo "cannot guess after the game is lost" + fi + + if [[ -n $guess && $word =~ $guess && ! $masked_word =~ $guess ]]; then + for (( i = 0; i < ${#word}; i++ )); do + if [[ ${word:i:1} == "$guess" ]]; then + masked_word="${masked_word:0:i}${guess}${masked_word:i+1}" + fi + done + if [[ ! $masked_word =~ "_" ]]; then + state="Win" + fi + elif [[ -n $guess ]]; then + if (( remaining_failures == 0 )); then + state="Lose" + else + (( remaining_failures -- )) + fi + fi + done + + echo "State: $state" + echo "Masked word: $masked_word" + echo "Remaining failures: $remaining_failures" +} + +guess "$@" diff --git a/exercises/practice/save-the-cow/.meta/template.j2 b/exercises/practice/save-the-cow/.meta/template.j2 new file mode 100644 index 00000000..f1c7864a --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/template.j2 @@ -0,0 +1,15 @@ +{{ header }} +{% for case in cases %} +@test "{{ case["descriptions"] | join(": ") }}" { + {% if loop.first %}# {% endif %}[[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash {{ solution }} "{{ case["input"]["word"] }}" "{{ case["input"]["guesses"] | join('" "') }}" + assert_success +{%- if case["expect_error"] %} + assert_output --partial "{{ case["expect_error_msg"] }}" +{%- else %} + assert_output --partial "{{ case["expected"]["state"] }}" + assert_output --partial "{{ case["expected"]["maskedWord"] }}" + assert_output --partial "{{ case["expected"]["remainingFailures"] }}" +{%- endif %} +} +{% endfor %} diff --git a/exercises/practice/save-the-cow/bats-extra.bash b/exercises/practice/save-the-cow/bats-extra.bash new file mode 100644 index 00000000..7cef63ea --- /dev/null +++ b/exercises/practice/save-the-cow/bats-extra.bash @@ -0,0 +1,642 @@ +# This is the source code for bats-support and bats-assert, concatenated +# * https://github.com/bats-core/bats-support +# * https://github.com/bats-core/bats-assert +# +# Comments have been removed to save space. See the git repos for full source code. + +############################################################ +# +# bats-support - Supporting library for Bats test helpers +# +# Written in 2016 by Zoltan Tombol +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any +# warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# . +# + +# shellcheck disable=SC2120 # (warning): fail references arguments, but none are ever passed. +fail() { + # shellcheck disable=2015 # (info): Note that A && B || C is not if-then-else. C may run when A is true. + (( $# == 0 )) && batslib_err || batslib_err "$@" + return 1 +} + +batslib_is_caller() { + local -i is_mode_direct=1 + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -i|--indirect) is_mode_direct=0; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + # Arguments. + local -r func="$1" + + # Check call stack. + if (( is_mode_direct )); then + [[ $func == "${FUNCNAME[2]}" ]] && return 0 + else + local -i depth + for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do + [[ $func == "${FUNCNAME[$depth]}" ]] && return 0 + done + fi + + return 1 +} + +batslib_err() { + { if (( $# > 0 )); then + echo "$@" + else + cat - + fi + } >&2 +} + +batslib_count_lines() { + local -i n_lines=0 + local line + while IFS='' read -r line || [[ -n $line ]]; do + (( ++n_lines )) + done < <(printf '%s' "$1") + echo "$n_lines" +} + +batslib_is_single_line() { + for string in "$@"; do + (( $(batslib_count_lines "$string") > 1 )) && return 1 + done + return 0 +} + +batslib_get_max_single_line_key_width() { + local -i max_len=-1 + while (( $# != 0 )); do + local -i key_len="${#1}" + batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len" + shift 2 + done + echo "$max_len" +} + +batslib_print_kv_single() { + local -ir col_width="$1"; shift + while (( $# != 0 )); do + printf '%-*s : %s\n' "$col_width" "$1" "$2" + shift 2 + done +} + +batslib_print_kv_multi() { + while (( $# != 0 )); do + printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )" + printf '%s\n' "$2" + shift 2 + done +} + +batslib_print_kv_single_or_multi() { + local -ir width="$1"; shift + local -a pairs=( "$@" ) + + local -a values=() + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + values+=( "${pairs[$i]}" ) + done + + if batslib_is_single_line "${values[@]}"; then + batslib_print_kv_single "$width" "${pairs[@]}" + else + local -i i + for (( i=1; i < ${#pairs[@]}; i+=2 )); do + pairs[i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )" + done + batslib_print_kv_multi "${pairs[@]}" + fi +} + +batslib_prefix() { + local -r prefix="${1:- }" + local line + while IFS='' read -r line || [[ -n $line ]]; do + printf '%s%s\n' "$prefix" "$line" + done +} + +batslib_mark() { + local -r symbol="$1"; shift + # Sort line numbers. + # shellcheck disable=SC2046 # (warning): Quote this to prevent word splitting. + set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" ) + + local line + local -i idx=0 + while IFS='' read -r line || [[ -n $line ]]; do + if (( ${1:--1} == idx )); then + printf '%s\n' "${symbol}${line:${#symbol}}" + shift + else + printf '%s\n' "$line" + fi + (( ++idx )) + done +} + +batslib_decorate() { + echo + echo "-- $1 --" + cat - + echo '--' + echo +} + +############################################################ + +assert() { + if ! "$@"; then + batslib_print_kv_single 10 'expression' "$*" \ + | batslib_decorate 'assertion failed' \ + | fail + fi +} + +assert_equal() { + if [[ $1 != "$2" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$2" \ + 'actual' "$1" \ + | batslib_decorate 'values do not equal' \ + | fail + fi +} + +assert_failure() { + : "${output?}" + : "${status?}" + + (( $# > 0 )) && local -r expected="$1" + if (( status == 0 )); then + batslib_print_kv_single_or_multi 6 'output' "$output" \ + | batslib_decorate 'command succeeded, but it was expected to fail' \ + | fail + elif (( $# > 0 )) && (( status != expected )); then + { local -ir width=8 + batslib_print_kv_single "$width" \ + 'expected' "$expected" \ + 'actual' "$status" + batslib_print_kv_single_or_multi "$width" \ + 'output' "$output" + } \ + | batslib_decorate 'command failed as expected, but status differs' \ + | fail + fi +} + +assert_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + : "${lines?}" + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -n|--index) + if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + + # Arguments. + local -r expected="$1" + + if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_line' \ + | fail + return $? + fi + + # Matching. + if (( is_match_line )); then + # Specific line. + if (( is_mode_regexp )); then + if ! [[ ${lines[$idx]} =~ $expected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression does not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} != *"$expected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$expected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line does not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} != "$expected" ]]; then + batslib_print_kv_single 8 \ + 'index' "$idx" \ + 'expected' "$expected" \ + 'actual' "${lines[$idx]}" \ + | batslib_decorate 'line differs' \ + | fail + fi + fi + else + # Contained in output. + if (( is_mode_regexp )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} =~ $expected ]] && return 0 + done + { local -ar single=( 'regexp' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'no output line matches regular expression' \ + | fail + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == *"$expected"* ]] && return 0 + done + { local -ar single=( 'substring' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'no output line contains substring' \ + | fail + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + [[ ${lines[$idx]} == "$expected" ]] && return 0 + done + { local -ar single=( 'line' "$expected" ) + local -ar may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}" + } \ + | batslib_decorate 'output does not contain line' \ + | fail + fi + fi +} + +assert_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_nonempty=0 + local -i use_stdin=0 + : "${output?}" + + # Handle options. + if (( $# == 0 )); then + is_mode_nonempty=1 + fi + + while (( $# > 0 )); do + case "$1" in + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + -|--stdin) use_stdin=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + return $? + fi + + # Arguments. + local expected + if (( use_stdin )); then + expected="$(cat -)" + else + expected="${1-}" + fi + + # Matching. + if (( is_mode_nonempty )); then + if [ -z "$output" ]; then + echo 'expected non-empty output, but output was empty' \ + | batslib_decorate 'no output' \ + | fail + fi + elif (( is_mode_regexp )); then + # shellcheck disable=SC2319 # (warning) `$?` in a conditional expression: https://github.com/koalaman/shellcheck/wiki/SC2319 + if [[ '' =~ $expected ]] || (( $? == 2 )); then + # the regex matches an empty string or it is syntactically incorrect. + echo "Invalid extended regular expression: \`$expected'" \ + | batslib_decorate 'ERROR: assert_output' \ + | fail + elif ! [[ $output =~ $expected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression does not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output != *"$expected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$expected" \ + 'output' "$output" \ + | batslib_decorate 'output does not contain substring' \ + | fail + fi + else + if [[ $output != "$expected" ]]; then + batslib_print_kv_single_or_multi 8 \ + 'expected' "$expected" \ + 'actual' "$output" \ + | batslib_decorate 'output differs' \ + | fail + fi + fi +} + +assert_success() { + : "${output?}" + : "${status?}" + + if (( status != 0 )); then + { local -ir width=6 + batslib_print_kv_single "$width" 'status' "$status" + batslib_print_kv_single_or_multi "$width" 'output' "$output" + } \ + | batslib_decorate 'command failed' \ + | fail + fi +} + +refute() { + if "$@"; then + batslib_print_kv_single 10 'expression' "$*" \ + | batslib_decorate 'assertion succeeded, but it was expected to fail' \ + | fail + fi +} + +refute_line() { + local -i is_match_line=0 + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + : "${lines?}" + + # Handle options. + while (( $# > 0 )); do + case "$1" in + -n|--index) + if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then + echo "\`--index' requires an integer argument: \`$2'" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + is_match_line=1 + local -ri idx="$2" + shift 2 + ;; + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + + # Arguments. + local -r unexpected="$1" + + if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_line' \ + | fail + return $? + fi + + # Matching. + if (( is_match_line )); then + # Specific line. + if (( is_mode_regexp )); then + if [[ ${lines[$idx]} =~ $unexpected ]]; then + batslib_print_kv_single 6 \ + 'index' "$idx" \ + 'regexp' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'regular expression should not match line' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + batslib_print_kv_single 9 \ + 'index' "$idx" \ + 'substring' "$unexpected" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should not contain substring' \ + | fail + fi + else + if [[ ${lines[$idx]} == "$unexpected" ]]; then + batslib_print_kv_single 5 \ + 'index' "$idx" \ + 'line' "${lines[$idx]}" \ + | batslib_decorate 'line should differ' \ + | fail + fi + fi + else + # Line contained in output. + if (( is_mode_regexp )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} =~ $unexpected ]]; then + { local -ar single=( 'regexp' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'no line should match the regular expression' \ + | fail + return $? + fi + done + elif (( is_mode_partial )); then + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == *"$unexpected"* ]]; then + { local -ar single=( 'substring' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'no line should contain substring' \ + | fail + return $? + fi + done + else + local -i idx + for (( idx = 0; idx < ${#lines[@]}; ++idx )); do + if [[ ${lines[$idx]} == "$unexpected" ]]; then + { local -ar single=( 'line' "$unexpected" 'index' "$idx" ) + local -a may_be_multi=( 'output' "$output" ) + local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )" + batslib_print_kv_single "$width" "${single[@]}" + if batslib_is_single_line "${may_be_multi[1]}"; then + batslib_print_kv_single "$width" "${may_be_multi[@]}" + else + may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )" + batslib_print_kv_multi "${may_be_multi[@]}" + fi + } \ + | batslib_decorate 'line should not be in output' \ + | fail + return $? + fi + done + fi + fi +} + +refute_output() { + local -i is_mode_partial=0 + local -i is_mode_regexp=0 + local -i is_mode_empty=0 + local -i use_stdin=0 + : "${output?}" + + # Handle options. + if (( $# == 0 )); then + is_mode_empty=1 + fi + + while (( $# > 0 )); do + case "$1" in + -p|--partial) is_mode_partial=1; shift ;; + -e|--regexp) is_mode_regexp=1; shift ;; + -|--stdin) use_stdin=1; shift ;; + --) shift; break ;; + *) break ;; + esac + done + + if (( is_mode_partial )) && (( is_mode_regexp )); then + echo "\`--partial' and \`--regexp' are mutually exclusive" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Arguments. + local unexpected + if (( use_stdin )); then + unexpected="$(cat -)" + else + unexpected="${1-}" + fi + + if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then + echo "Invalid extended regular expression: \`$unexpected'" \ + | batslib_decorate 'ERROR: refute_output' \ + | fail + return $? + fi + + # Matching. + if (( is_mode_empty )); then + if [ -n "$output" ]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output non-empty, but expected no output' \ + | fail + fi + elif (( is_mode_regexp )); then + if [[ $output =~ $unexpected ]]; then + batslib_print_kv_single_or_multi 6 \ + 'regexp' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'regular expression should not match output' \ + | fail + fi + elif (( is_mode_partial )); then + if [[ $output == *"$unexpected"* ]]; then + batslib_print_kv_single_or_multi 9 \ + 'substring' "$unexpected" \ + 'output' "$output" \ + | batslib_decorate 'output should not contain substring' \ + | fail + fi + else + if [[ $output == "$unexpected" ]]; then + batslib_print_kv_single_or_multi 6 \ + 'output' "$output" \ + | batslib_decorate 'output equals, but it was expected to differ' \ + | fail + fi + fi +} diff --git a/exercises/practice/save-the-cow/save_the_cow.bats b/exercises/practice/save-the-cow/save_the_cow.bats index e69de29b..619c169e 100644 --- a/exercises/practice/save-the-cow/save_the_cow.bats +++ b/exercises/practice/save-the-cow/save_the_cow.bats @@ -0,0 +1,91 @@ +#!/usr/bin/env bats +load bats-extra + +# generated on 2026-09-12T19:25:52+00:00 + +@test "Initially 9 failures are allowed and no letters are guessed" { + # [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "" + assert_success + assert_output --partial "Ongoing" + assert_output --partial "____" + assert_output --partial "9" +} + +@test "After 10 failures the game is over" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" + assert_success + assert_output --partial "Lose" + assert_output --partial "____" + assert_output --partial "0" +} + +@test "Losing with several correct guesses" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "t" "o" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" + assert_success + assert_output --partial "Lose" + assert_output --partial "_oot" + assert_output --partial "0" +} + +@test "Feeding a correct letter removes underscores" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "t" + assert_success + assert_output --partial "Ongoing" + assert_output --partial "___t" + assert_output --partial "9" +} + +@test "Feeding a correct letter twice counts as a failure" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "t" "t" + assert_success + assert_output --partial "Ongoing" + assert_output --partial "___t" + assert_output --partial "8" +} + +@test "Guessing a repeated letter reveals all instances" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "t" "t" "o" + assert_success + assert_output --partial "Ongoing" + assert_output --partial "_oot" + assert_output --partial "8" +} + +@test "Getting all the letters right makes for a win" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "t" "t" "o" "l" + assert_success + assert_output --partial "Win" + assert_output --partial "loot" + assert_output --partial "8" +} + +@test "Winning on the last guess is still a win" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "a" "b" "c" "d" "e" "f" "g" "h" "i" "t" "o" "l" + assert_success + assert_output --partial "Win" + assert_output --partial "loot" + assert_output --partial "0" +} + +@test "Guessing after a lose is error" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" + assert_success + assert_output --partial "cannot guess after the game is lost" +} + +@test "Guessing after a win is error" { + [[ $BATS_RUN_SKIPPED == "true" ]] || skip + run bash save_the_cow.sh "loot" "t" "o" "l" "l" + assert_success + assert_output --partial "cannot guess after the game is won" +} + diff --git a/exercises/practice/save-the-cow/save_the_cow.sh b/exercises/practice/save-the-cow/save_the_cow.sh index e69de29b..960869f3 100644 --- a/exercises/practice/save-the-cow/save_the_cow.sh +++ b/exercises/practice/save-the-cow/save_the_cow.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# The following comments should help you get started: +# - Bash is flexible. You may use functions or write a "raw" script. +# +# - Complex code can be made easier to read by breaking it up +# into functions, however this is sometimes overkill in bash. +# +# - You can find links about good style and other resources +# for Bash in './README.md'. It came with this exercise. +# +# Example: +# # other functions here +# # ... +# # ... +# +# main () { +# # your main function code here +# } +# +# # call main with all of the positional arguments +# main "$@" +# +# *** PLEASE REMOVE THESE COMMENTS BEFORE SUBMITTING YOUR SOLUTION *** From 810c0f7058855dc50a9d24036480b9dd16e43b85 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Sat, 12 Sep 2026 22:46:15 +0200 Subject: [PATCH 3/6] Set the difficulty rank to `3` --- config.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config.json b/config.json index aa4b57ba..d526a31d 100644 --- a/config.json +++ b/config.json @@ -556,6 +556,14 @@ "string_transformation" ] }, + { + "slug": "save-the-cow", + "name": "Save the Cow", + "uuid": "21b2a344-925e-427a-a2bb-69122df2ce61", + "practices": [], + "prerequisites": [], + "difficulty": 3 + }, { "slug": "sieve", "name": "Sieve", @@ -1244,14 +1252,6 @@ "associative_arrays", "control_flow_conditionals" ] - }, - { - "slug": "save-the-cow", - "name": "Save the Cow", - "uuid": "21b2a344-925e-427a-a2bb-69122df2ce61", - "practices": [], - "prerequisites": [], - "difficulty": 1 } ], "foregone": [ From bf95978253df65b1d34d55113b0f02755fccbdd9 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Sat, 12 Sep 2026 22:49:09 +0200 Subject: [PATCH 4/6] Added GitHub handle to `config.json` --- exercises/practice/save-the-cow/.meta/config.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exercises/practice/save-the-cow/.meta/config.json b/exercises/practice/save-the-cow/.meta/config.json index 4d7a536c..0a529ef9 100644 --- a/exercises/practice/save-the-cow/.meta/config.json +++ b/exercises/practice/save-the-cow/.meta/config.json @@ -1,5 +1,7 @@ { - "authors": [], + "authors": [ + "resu-xuniL" + ], "files": { "solution": [ "save_the_cow.sh" From 145bf571270981a2a0cd7036fbbe78be0f0264c2 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Sun, 13 Sep 2026 20:07:34 +0200 Subject: [PATCH 5/6] Change error tests with `assert_failure` --- exercises/practice/save-the-cow/.meta/example.sh | 6 ++++-- exercises/practice/save-the-cow/.meta/template.j2 | 5 +++-- exercises/practice/save-the-cow/save_the_cow.bats | 10 +++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/exercises/practice/save-the-cow/.meta/example.sh b/exercises/practice/save-the-cow/.meta/example.sh index e718f49c..74cea1a6 100644 --- a/exercises/practice/save-the-cow/.meta/example.sh +++ b/exercises/practice/save-the-cow/.meta/example.sh @@ -10,9 +10,11 @@ guess() { for guess in "${guesses[@]}"; do if [[ $state == "Win" ]]; then - echo "cannot guess after the game is won" + echo "cannot guess after the game is won" >&2 + exit 1 elif [[ $state == "Lose" ]]; then - echo "cannot guess after the game is lost" + echo "cannot guess after the game is lost" >&2 + exit 1 fi if [[ -n $guess && $word =~ $guess && ! $masked_word =~ $guess ]]; then diff --git a/exercises/practice/save-the-cow/.meta/template.j2 b/exercises/practice/save-the-cow/.meta/template.j2 index f1c7864a..75d64b3b 100644 --- a/exercises/practice/save-the-cow/.meta/template.j2 +++ b/exercises/practice/save-the-cow/.meta/template.j2 @@ -3,10 +3,11 @@ @test "{{ case["descriptions"] | join(": ") }}" { {% if loop.first %}# {% endif %}[[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash {{ solution }} "{{ case["input"]["word"] }}" "{{ case["input"]["guesses"] | join('" "') }}" - assert_success {%- if case["expect_error"] %} - assert_output --partial "{{ case["expect_error_msg"] }}" + assert_failure + assert_output "{{ case["expect_error_msg"] }}" {%- else %} + assert_success assert_output --partial "{{ case["expected"]["state"] }}" assert_output --partial "{{ case["expected"]["maskedWord"] }}" assert_output --partial "{{ case["expected"]["remainingFailures"] }}" diff --git a/exercises/practice/save-the-cow/save_the_cow.bats b/exercises/practice/save-the-cow/save_the_cow.bats index 619c169e..583ca715 100644 --- a/exercises/practice/save-the-cow/save_the_cow.bats +++ b/exercises/practice/save-the-cow/save_the_cow.bats @@ -1,7 +1,7 @@ #!/usr/bin/env bats load bats-extra -# generated on 2026-09-12T19:25:52+00:00 +# generated on 2026-09-13T17:59:07+00:00 @test "Initially 9 failures are allowed and no letters are guessed" { # [[ $BATS_RUN_SKIPPED == "true" ]] || skip @@ -78,14 +78,14 @@ load bats-extra @test "Guessing after a lose is error" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" - assert_success - assert_output --partial "cannot guess after the game is lost" + assert_failure + assert_output "cannot guess after the game is lost" } @test "Guessing after a win is error" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "t" "o" "l" "l" - assert_success - assert_output --partial "cannot guess after the game is won" + assert_failure + assert_output "cannot guess after the game is won" } From cbcec9afe03cc22a63d4aac0b8f504284efe8b53 Mon Sep 17 00:00:00 2001 From: resu-xuniL Date: Sun, 13 Sep 2026 21:25:13 +0200 Subject: [PATCH 6/6] Change for more concrete expectations --- .../practice/save-the-cow/.meta/example.sh | 4 +- .../practice/save-the-cow/.meta/template.j2 | 11 ++- .../practice/save-the-cow/save_the_cow.bats | 90 +++++++++++++------ 3 files changed, 74 insertions(+), 31 deletions(-) diff --git a/exercises/practice/save-the-cow/.meta/example.sh b/exercises/practice/save-the-cow/.meta/example.sh index 74cea1a6..b4b1929d 100644 --- a/exercises/practice/save-the-cow/.meta/example.sh +++ b/exercises/practice/save-the-cow/.meta/example.sh @@ -35,9 +35,7 @@ guess() { fi done - echo "State: $state" - echo "Masked word: $masked_word" - echo "Remaining failures: $remaining_failures" + printf "State: %s\nMasked word: %s\nRemaining failures: %s" "$state" "$masked_word" "$remaining_failures" } guess "$@" diff --git a/exercises/practice/save-the-cow/.meta/template.j2 b/exercises/practice/save-the-cow/.meta/template.j2 index 75d64b3b..a95eca34 100644 --- a/exercises/practice/save-the-cow/.meta/template.j2 +++ b/exercises/practice/save-the-cow/.meta/template.j2 @@ -7,10 +7,15 @@ assert_failure assert_output "{{ case["expect_error_msg"] }}" {%- else %} + expected="$(cat << EXPECTED +State: {{ case["expected"]["state"] }} +Masked word: {{ case["expected"]["maskedWord"] }} +Remaining failures: {{ case["expected"]["remainingFailures"] }} +EXPECTED + )" + assert_success - assert_output --partial "{{ case["expected"]["state"] }}" - assert_output --partial "{{ case["expected"]["maskedWord"] }}" - assert_output --partial "{{ case["expected"]["remainingFailures"] }}" + assert_output "$expected" {%- endif %} } {% endfor %} diff --git a/exercises/practice/save-the-cow/save_the_cow.bats b/exercises/practice/save-the-cow/save_the_cow.bats index 583ca715..e3ff5375 100644 --- a/exercises/practice/save-the-cow/save_the_cow.bats +++ b/exercises/practice/save-the-cow/save_the_cow.bats @@ -1,78 +1,118 @@ #!/usr/bin/env bats load bats-extra -# generated on 2026-09-13T17:59:07+00:00 +# generated on 2026-09-13T19:12:55+00:00 @test "Initially 9 failures are allowed and no letters are guessed" { # [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "" + expected="$(cat << EXPECTED +State: Ongoing +Masked word: ____ +Remaining failures: 9 +EXPECTED + )" + assert_success - assert_output --partial "Ongoing" - assert_output --partial "____" - assert_output --partial "9" + assert_output "$expected" } @test "After 10 failures the game is over" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" + expected="$(cat << EXPECTED +State: Lose +Masked word: ____ +Remaining failures: 0 +EXPECTED + )" + assert_success - assert_output --partial "Lose" - assert_output --partial "____" - assert_output --partial "0" + assert_output "$expected" } @test "Losing with several correct guesses" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "t" "o" "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" + expected="$(cat << EXPECTED +State: Lose +Masked word: _oot +Remaining failures: 0 +EXPECTED + )" + assert_success - assert_output --partial "Lose" - assert_output --partial "_oot" - assert_output --partial "0" + assert_output "$expected" } @test "Feeding a correct letter removes underscores" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "t" + expected="$(cat << EXPECTED +State: Ongoing +Masked word: ___t +Remaining failures: 9 +EXPECTED + )" + assert_success - assert_output --partial "Ongoing" - assert_output --partial "___t" - assert_output --partial "9" + assert_output "$expected" } @test "Feeding a correct letter twice counts as a failure" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "t" "t" + expected="$(cat << EXPECTED +State: Ongoing +Masked word: ___t +Remaining failures: 8 +EXPECTED + )" + assert_success - assert_output --partial "Ongoing" - assert_output --partial "___t" - assert_output --partial "8" + assert_output "$expected" } @test "Guessing a repeated letter reveals all instances" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "t" "t" "o" + expected="$(cat << EXPECTED +State: Ongoing +Masked word: _oot +Remaining failures: 8 +EXPECTED + )" + assert_success - assert_output --partial "Ongoing" - assert_output --partial "_oot" - assert_output --partial "8" + assert_output "$expected" } @test "Getting all the letters right makes for a win" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "t" "t" "o" "l" + expected="$(cat << EXPECTED +State: Win +Masked word: loot +Remaining failures: 8 +EXPECTED + )" + assert_success - assert_output --partial "Win" - assert_output --partial "loot" - assert_output --partial "8" + assert_output "$expected" } @test "Winning on the last guess is still a win" { [[ $BATS_RUN_SKIPPED == "true" ]] || skip run bash save_the_cow.sh "loot" "a" "b" "c" "d" "e" "f" "g" "h" "i" "t" "o" "l" + expected="$(cat << EXPECTED +State: Win +Masked word: loot +Remaining failures: 0 +EXPECTED + )" + assert_success - assert_output --partial "Win" - assert_output --partial "loot" - assert_output --partial "0" + assert_output "$expected" } @test "Guessing after a lose is error" {