用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Texarkanine/.cursor-rules --skill shell-tdd命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | shell-tdd |
| description | Required test-driven development practice for writing shell scripts |
This rule defines best practices for AI assistants to follow when writing shell scripts using test-driven development (TDD). The AI should create shell scripts that are testable with shunit2, separate concerns, avoid side effects when being sourced, and allow functions to be tested in isolation. Following these guidelines ensures scripts can be reliably tested and maintained.
When asked to create shell scripts, the AI should follow this test-driven workflow:
# STEP 1: First write the test file (test_calculator.sh)
#!/bin/sh
# Source script under test
. "$(dirname "${0}")/../calculator.sh"
# Test for add function
test_add() {
result=$(add 5 3)
assertEquals "Addition should work correctly" "8" "$result"
}
# Test for subtract function
test_subtract() {
result=$(subtract 10 4)
assertEquals "Subtraction should work correctly" "6" "$result"
}
# Load shunit2
. "$(dirname "${0}")/shunit2"
# STEP 2: Then implement the minimum functionality to pass the tests (calculator.sh)
#!/bin/sh
# Add two numbers
add() {
echo $(($1 + $2))
}
# Subtract second number from first
subtract() {
echo $(($1 - $2))
}
# Only run if executed directly
if [ "${0##*/}" = "calculator.sh" ]; then
echo "Calculator utility"
fi
Always protect the main execution code with a conditional that checks if the script is being executed directly:
# Define all functions first
# Only run main code if script is executed, not sourced
if [ "${0##*/}" = "script_name.sh" ]; then
main "$@" # Call a main function with all arguments
fi
Where "script_name.sh" is the actual filename of your script. This works because:
$0 contains the script path/name$0 contains the name of the parent script doing the sourcingThis allows your script to be sourced for testing without executing its main behavior.
Structure scripts as collections of well-defined functions:
#!/bin/sh
# Example of a testable shell script
# Clear function documentation
# Adds two numbers
# Arguments:
# $1 - First number
# $2 - Second number
# Returns:
# Sum of the two numbers
add_numbers() {
echo $(( $1 + $2 ))
}
# Main function that orchestrates execution
main() {
local result
result="$(add_numbers 5 10)"
echo "The result is: ${result}"
}
# Only run if executed directly (assuming script is named example.sh)
if [ "${0##*/}" = "example.sh" ]; then
main "$@"
fi
exit outside of the main execution block# BAD - will terminate the test suite if sourced
validate_input() {
if [ -z "${1}" ]; then
echo "Error: Input required" >&2
exit 1 # This will exit the test suite!
fi
}
# GOOD - returns error code instead
validate_input() {
if [ -z "${1}" ]; then
echo "Error: Input required" >&2
return 1 # Return error code instead
fi
return 0
}
Save and restore environment state when necessary:
backup_file() {
# Save original state
_original_dir="$(pwd)"
# Perform operation
cd "${1}" || return 1
cp "${2}" "${2}.bak" || return 1
# Restore original state
cd "${_original_dir}" || return 1
return 0
}
Make input/output operations testable by:
# Hard to test - uses hardcoded file
process_data() {
cat /etc/config.conf | grep "pattern"
}
# Testable - accepts input source as parameter
process_data() {
config_file="${1:-/etc/config.conf}"
grep "pattern" "${config_file}"
}
Design for easy mocking of external commands in tests:
# Define how external commands are called
git_sync() {
git clone "${1}" "${2}" || return 1
return 0
}
# Use the function instead of calling git directly
update_repo() {
repo_url="${1}"
target_dir="${2}"
git_sync "${repo_url}" "${target_dir}"
}
# In tests, you can mock git_sync:
# git_sync() { echo "Mock: git clone ${1} ${2}"; return 0; }
When the AI generates a shell script, it should:
project/
├── scripts/
│ └── example.sh
└── tests/
├── common.sh
└── unit/
└── example_test.sh
# In tests/common.sh
source_script() {
# Save original environment if needed
_ORIGINAL_ENV_VARS="$(env)"
# Source the script
# shellcheck disable=SC1090
. "${1}"
# Verify it was sourced correctly
if [ $? -ne 0 ]; then
echo "Error: Failed to source ${1}" >&2
return 1
fi
return 0
}
#!/bin/sh
# Test file: test_example.sh
# Load common test utilities
. "$(dirname "${0}")/common.sh"
# Source the script under test
source_script "$(dirname "${0}")/../example.sh"
# Test function
test_add_numbers() {
result="$(add_numbers 5 7)"
assertEquals "Addition should work correctly" "12" "${result}"
}
# Load and run shunit2
. "$(dirname "${0}")/shunit2"
Unconditional exits: Using exit outside the main execution block will terminate test suites
# AVOID THIS - will break tests
cleanup() {
if [ ! -f "${1}" ]; then
echo "Error: File not found" >&2
exit 1 # BAD - will exit test suite
fi
}
Hardcoded environment assumptions: Making assumptions about the environment
# AVOID THIS - assumes current directory
process_files() {
for file in *.txt; do # Bad - depends on current directory
process_file "${file}"
done
}
# BETTER - accepts directory as parameter
process_files() {
dir="${1:-.}" # Default to current directory but allows override
for file in "${dir}"/*.txt; do
process_file "${file}"
done
}
Global state modifications: Modifying global state without restoring it
# AVOID THIS - changes global state without restoring
set_environment() {
set -e # Will affect test environment
DEBUG=
}
() {
_original_debug=
DEBUG=
() {
DEBUG=
}
}
When implementing shell scripts, always:
#!/bin/sh
# tests/unit/functions_test.sh
# Load common test utilities
. "$(dirname "${0}")/../common.sh"
# Set up test environment
setUp() {
# Create temporary test directory
TEST_DIR="$(mktemp -d)"
cd "${TEST_DIR}" || fail "Failed to change to test directory"
# Create test files
echo "test content" > test_file.txt
}
# Clean up test environment
tearDown() {
# Return to original directory
cd / || fail "Failed to change directory"
# Remove test directory
rm -rf "${TEST_DIR}"
}
# Source the script under test
source_script "$(dirname "${0}")/../../my_script.sh"
# Test backup_file function
test_backup_file() {
# Call function
backup_file "${TEST_DIR}" "test_file.txt"
# Assert backup was created
assertTrue "Backup file should exist" "[ -f 'test_file.txt.bak' ]"
# Assert content was preserved
assertEquals "Backup content should match original" \
"$(cat test_file.txt)"
}
.
Untestable output: Writing directly to stdout/stderr without redirection options
# AVOID THIS - direct output hard to test
display_status() {
echo "Status: ${1}"
}
# BETTER - allows output redirection
display_status() {
echo "Status: ${1}" >&${2:-1} # Default to stdout but allows redirection
}
Test function exit codes: In shunit2 test functions, the exit code of the last command becomes the function's return value
# AVOID THIS - grep failure becomes test failure
test_something() {
# ... test code ...
grep -q "pattern" "$file" && fail "Pattern should not exist"
# If grep doesn't find pattern, it returns 1, which shunit2 interprets as test failure
}
# BETTER - explicitly return success
test_something() {
# ... test code ...
grep -q "pattern" "$file" && fail "Pattern should not exist"
return 0 # Explicitly return success to prevent false failures
}
Key point: Always end test functions with return 0 & rely on explicit invocations of fail to surface test failures.