Patterns¶
The Taskfile from the specification is very basic. Commands can also have parameters and call each other.
The following is a collection of advanced Taskfile patterns.
Load dotenv variables.¶
Load environment variables from the .env file.
Set default parameter¶
Fallback to a default value for a parameter.
Ensure parameter is not empty¶
Check the first param and exit if it is empty.
Prompt for input¶
Use read to ask for inputs.
if [[ -z "$2" ]]; then
read -p 'Enter the task description: ' task_description
else
task_description="$2"
fi
Static env vars¶
These vars are exported from the script and used in external tools.
Updateable vars¶
This variables can be configured from the environment, but is used only internally.
Conditional vars¶
Depending on the value of another var, this vars value changes.
Template with env vars¶
Create a parameterized file from a template. Requires envsubst.
template-with-env() {
echo "Template $CONFIGURATION_FILE"
export CONFIGURATION_1
export CONFIGURATION_2=${CONFIGURATION_2:=value}
envsubst < 'file.conf.template' > "$CONFIGURATION_FILE"
}
Create Python virtual env for git branch¶
Initialize Python virtual env with uv.
init-venv() {
GIT_BRANCH=$(git symbolic-ref --short -q HEAD)
if [[ ! -d ".venv$GIT_BRANCH" ]]; then
echo "Init .venv$GIT_BRANCH with $(uv --version)."
uv venv ".venv$GIT_BRANCH"
fi
}
Call a Python script¶
Run a Python script.
generate-password-hash() {
if [[ -z "$1" ]]; then echo "\$1 is empty."; exit; fi
export PASSWORD_PLAIN="$1"
bin/password-hash
}
The Python script: bin/password-hash
#!/usr/bin/env python3
import os
from passlib.context import CryptContext
crypt_context = CryptContext(schemes=['pbkdf2_sha512', 'plaintext'], deprecated=['plaintext'])
password = os.environ.get('PASSWORD_PLAIN')
print(crypt_context.hash(password))
Command with named parameters¶
Assuming you have a docker-compose.yml and would like to start selected or all containers.
start() {
if [[ "$1" =~ 'db' ]]; then
docker compose up -d db
fi
if [[ "$1" =~ 'admin' ]]; then
docker compose up -d admin
echo 'Open http://localhost:8000 url in your browser.'
fi
if [[ "$1" =~ 'odoo' ]]; then
docker compose up -d odoo
echo 'Open http://localhost:8069 url in your browser.'
fi
if [[ "$1" =~ 'mail' ]]; then
docker compose up -d mail
echo 'Open http://localhost:8025 url in your browser.'
fi
}
Start all containers with task start and selected with task start db,admin.
Run commands in container¶
Use docker exec -i to run commands in a container.
drop-db() {
local database="$1"
if [[ -z "$database" ]]; then
database="example"
fi
docker exec -i db psql "postgres://odoo:odoo@localhost:5432/postgres" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$database';"
docker exec -i db psql "postgres://odoo:odoo@localhost:5432/postgres" -c "DROP DATABASE \"$database\";"
}
Loop over files and folders¶
Use for to loop over file, folders or arrays.
render() {
echo "Update index.html for all folders"
for folder in ./*; do
if [[ -f "$folder/README.md" ]]; then
cd "$folder" || exit
md2html README.md _site/index.html
cd .. || exit
fi
done
}
Parse and convert a file¶
With this function you can split a file into multiple parts whenever a specific keyword is matching. In this example the keyword is !vault.
convert-vault-file() {
local file_path="$1"
local temp_file=$(mktemp)
local temp_part_file=$(mktemp)
local write_finished=false
local current_key=""
while IFS= read -r line; do
# Check for keyword
if [[ "$line" =~ "!vault" ]]; then
# Process previous vault entry if it exists
if [[ "$write_finished" = true ]] && [[ -n "$current_key" ]] && [[ -s "$temp_part_file" ]]; then
# Decrypt part file and write to assemble file
ansible-vault decrypt "$temp_part_file"
local value=$(cat "$temp_part_file")
echo "$current_key: $value" >> "$temp_file"
fi
# Set up for new vault entry
current_key=$(echo "$line" | cut -d':' -f1)
# Clear the file
: > "$temp_part_file"
# Flag as ready to write
write_finished=true
else
if [[ "$write_finished" = true ]]; then
# Pipe into part file
echo "$line" >> "$temp_part_file"
fi
done
done < "$file_path"
# Process the final vault entry
if [[ "$write_finished" = true ]] && [[ -n "$current_key" ]] && [[ -s "$temp_part_file" ]]; then
ansible-vault decrypt "$temp_part_file"
local value=$(cat "$temp_part_file")
echo "$current_key: $value" >> "$temp_file"
fi
# Output assembled file
cat "$temp_file"
# Cleanup temp files
rm -f "$temp_file"
rm -f "$temp_part_file"
}
Process data¶
This pattern tries to match the worklow of a jupyter notebook. Create a folder with the data processing scripts:
The following function lists the scripts and asks for the number of scripts to run. Every script until the entered number is concatenated and executed.
process-data() {
while true; do
local scripts=($(ls scripts))
echo -e "Available scripts:\n"
for index in "${!scripts[@]}"; do
echo "$((index+1)): ${scripts[$index]}"
done
echo ""
read -p "Enter the number of scripts to run (or 'q' to quit): " user_input
if [[ "$user_input" = "q" ]]; then
break
fi
local temp_file=$(mktemp)
for index in "${!scripts[@]}"; do
if (( $user_input >= $((index+1)) )); then
cat "scripts/${scripts[$index]}" >> "$temp_file"
echo >> "$temp_file"
fi
done
clear
echo -e "\nPython Output:\n\n---"
python "$temp_file"
local exit_code=$?
echo -e "---\n"
echo -e "Exit code: $exit_code\n"
rm "$temp_file"
done
}
Check commands¶
Add the following statements to check if the required commands is available:
if ! (command -v grep &> /dev/null && grep --version | grep -q GNU) && [[ "$OS_RELEASE" =~ ^(Darwin)$ ]]; then
echo 'Error: On Darwin systems, GNU grep must be installed.'
exit 1
fi
if ! command -v envsubst &> /dev/null; then
echo "Error: 'envsubst' is not available. Install 'gettext' package."
exit 1
fi
Load dotenv by git branch¶
If you want to have a .env.<branch> file for each git branch use this pattern: