|
1 | 1 | #!/usr/bin/env bash |
2 | 2 |
|
| 3 | +# Synopsis: |
| 4 | +# Scaffold the files for a new practice exercise. |
| 5 | +# After creating the exercise, follow the instructions in the output. |
| 6 | + |
| 7 | +# Example: |
| 8 | +# bin/add-exercise two-fer |
| 9 | + |
| 10 | +# Example with difficulty: |
| 11 | +# bin/add-exercise -d 5 two-fer |
| 12 | + |
| 13 | +# Example with author and difficulty: |
| 14 | +# bin/add-exercise -a foo -d 3 two-fer |
| 15 | + |
3 | 16 | set -euo pipefail |
| 17 | +scriptname=$0 |
4 | 18 |
|
5 | | -if [ $# -ne 1 ]; then |
6 | | - echo "Usage: bin/add-exercise <exercise-slug>" |
| 19 | +help_and_exit() { |
| 20 | + echo >&2 "Scaffold the files for a new practice exercise." |
| 21 | + echo >&2 "Usage: ${scriptname} [-h] [-a author] [-d difficulty] <exercise-slug>" |
| 22 | + echo >&2 "Where: author is the GitHub username of the exercise creator." |
| 23 | + echo >&2 "Where: difficulty is between 1 (easiest) to 10 (hardest)." |
7 | 24 | exit 1 |
8 | | -fi |
| 25 | +} |
9 | 26 |
|
10 | | -command -v jq >/dev/null 2>&1 || { |
11 | | - echo >&2 "jq is required but not installed. Please install it and make sure it's in your PATH." |
12 | | - exit 1 |
| 27 | +die() { echo >&2 "$*"; exit 1; } |
| 28 | + |
| 29 | +required_tool() { |
| 30 | + command -v "${1}" >/dev/null 2>&1 || |
| 31 | + die "${1} is required but not installed. Please install it and make sure it's in your PATH." |
13 | 32 | } |
14 | | -command -v curl >/dev/null 2>&1 || { |
15 | | - echo >&2 "curl is required but not installed. Please install it and make sure it's in your PATH." |
16 | | - exit 1 |
| 33 | + |
| 34 | +require_files_template() { |
| 35 | + jq -e --arg key "${1}" '.files[$key] | length > 0' config.json > /dev/null || |
| 36 | + die "The '.files.${1}' array in the 'config.json' file is empty. Please add at least one file. See https://exercism.org/docs/building/tracks/config-json#h-files for more information." |
17 | 37 | } |
18 | 38 |
|
19 | | -bin/fetch-configlet |
| 39 | +required_tool jq |
| 40 | + |
| 41 | +require_files_template "solution" |
| 42 | +require_files_template "test" |
| 43 | +require_files_template "example" |
| 44 | + |
| 45 | +[[ -f ./bin/fetch-configlet ]] || die "Run this script from the repo's root directory." |
| 46 | + |
| 47 | +author='' |
| 48 | +difficulty='4' |
| 49 | +while getopts :ha:d: opt; do |
| 50 | + case $opt in |
| 51 | + h) help_and_exit ;; |
| 52 | + a) author=$OPTARG ;; |
| 53 | + d) difficulty=$OPTARG ;; |
| 54 | + ?) echo >&2 "Unknown option: -$OPTARG"; help_and_exit ;; |
| 55 | + esac |
| 56 | +done |
| 57 | +shift "$((OPTIND - 1))" |
| 58 | + |
| 59 | +(( $# >= 1 )) || help_and_exit |
20 | 60 |
|
21 | | -# Add entry for exercise in config.json |
22 | 61 | slug="${1}" |
23 | | -# shellcheck disable=SC2001 |
24 | | -name=$(echo "${slug}" | sed 's/\b\w/\u&/g') |
25 | | -uuid=$(bin/configlet uuid) |
26 | | -jq --arg slug "${slug}" --arg uuid "${uuid}" --arg name "${name}" \ |
27 | | - '.exercises.practice += [{slug: $slug, name: $name, uuid: $uuid, practices: [], prerequisites: [], difficulty: 1}]' \ |
28 | | - config.json | sed 's/"average_run_time": 5/"average_run_time": 5.0/' > config.json.tmp && mv config.json.tmp config.json |
29 | 62 |
|
30 | | -# Sync the exercise |
31 | | -bin/configlet sync --update --yes --tests include --filepaths --metadata --docs --exercise "${slug}" |
| 63 | +if [[ -z "${author}" ]]; then |
| 64 | + read -rp 'Your GitHub username: ' author |
| 65 | +fi |
| 66 | + |
| 67 | +./bin/fetch-configlet |
| 68 | +./bin/configlet create --practice-exercise "${slug}" --author "${author}" --difficulty "${difficulty}" |
| 69 | + |
| 70 | +filter='.exercises.practice = (.exercises.practice | sort_by(.difficulty, .slug))' |
| 71 | +jq "${filter}" config.json > config.sorted && mv config.sorted config.json |
32 | 72 |
|
33 | 73 | exercise_dir="exercises/practice/${slug}" |
34 | | -exercise_files=$(jq -r '.files[][]' "${exercise_dir}/.meta/config.json") |
| 74 | +files=$(jq -r --arg dir "${exercise_dir}" '.files | to_entries | map({key: .key, value: (.value | map("'"'"'" + $dir + "/" + . + "'"'"'") | join(" and "))}) | from_entries' "${exercise_dir}/.meta/config.json") |
35 | 75 |
|
36 | | -for file in ${exercise_files}; do |
37 | | - touch "${exercise_dir}/${file}" |
38 | | -done |
| 76 | +cat << NEXT_STEPS |
| 77 | +
|
| 78 | +Your next steps are: |
| 79 | +- Create the test suite in $(jq -r '.test' <<< "${files}") |
| 80 | + - The tests should be based on the canonical data at 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json' |
| 81 | + - Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false" |
| 82 | +- Create the example solution in $(jq -r '.example' <<< "${files}") |
| 83 | +- Verify the example solution passes the tests by running 'bin/run-tests ${slug}' |
| 84 | +- Create the stub solution in $(jq -r '.solution' <<< "${files}") |
| 85 | +- Update the 'difficulty' value for the exercise's entry in the 'config.json' file in the repo's root |
| 86 | +- Validate CI using 'bin/configlet lint' and 'bin/configlet fmt' |
| 87 | +NEXT_STEPS |
0 commit comments