Skip to content

Instantly share code, notes, and snippets.

@eingruenesbeb
Last active June 25, 2024 13:50
Show Gist options
  • Save eingruenesbeb/72d57a260261e4b88a0ca16714dcda3b to your computer and use it in GitHub Desktop.
Save eingruenesbeb/72d57a260261e4b88a0ca16714dcda3b to your computer and use it in GitHub Desktop.
A "little" Bash script to generate an empty Minecraft datapack
#!/bin/bash
# This script will generate an empty datapack with the basic files needed
# It prompts the user to input details like parent directory, datapack name, format, and description.
# global variables: use_gum, parent_dir, name, pack_format, description, selected_features, subdirectories
# Helper function to join array elements with a specified delimiter
function joinArrayWith() {
local delimiter="$1"
shift
local joined
joined=$(printf "%s${delimiter}" "$@")
echo "${joined%"${delimiter}"}"
}
function gatherPackFeatures() {
# Possible datapack-features by type:
local features_4=("Functions" "Structures" "Tags" "Advancements" "Item modifier" "Loot tables" "Predicates" "Recipes")
local features_5=("Dimensions" "Dimension types" "Trim materials" "Trim patterns" "Customized worldgeneration")
local features_10=("Chat types")
local features_12=("Damage types")
local features_34=("Banner patterns" "Wolf variants")
local features_42=("Enchantments" "Enchantment providers" "Paintings")
# Concatenate every available feature for the datapack format
local possible_features=("${features_4[@]}")
if [[ "$pack_format" -ge 5 ]]; then
possible_features+=("${features_5[@]}")
fi
if [[ "$pack_format" -ge 10 ]]; then
possible_features+=("${features_10[@]}")
fi
if [[ "$pack_format" -ge 12 ]]; then
possible_features+=("${features_12[@]}")
fi
if [[ "$pack_format" -ge 34 ]]; then
possible_features+=("${features_34[@]}")
fi
if [[ "$pack_format" -ge 42 ]]; then
possible_features+=("${features_42[@]}")
fi
selected_features=()
if $use_gum; then
echo "What will your datapack include?"
# The features the user wants to include in the datapack
local selected
selected=$(gum choose --no-limit "${possible_features[@]}")
IFS=$'\n' read -r -d '' -a selected_features <<< "$selected"$'\n'
else
echo "What will your datapack include? Answer with a newline-separated set of: $(joinArrayWith ', ' "${possible_features[@]}")"
echo "Press \`<ctrl>+D\` on a new line to confirm."
local line
while read -r line; do
selected_features+=("$line")
done
fi
}
function detectGum() {
if [[ "$(command -v gum)" ]]; then
use_gum=true
else
use_gum=false
fi
}
# Function to generate the pack.mcmeta file content with provided pack format and description.
function generatePackMeta() {
cat << EOF
{
"pack": {
"pack_format": $1,
"description": "$2"
}
}
EOF
}
# Function to generate the load.json file content with the given namespace.
function generateLoadTag() {
cat << EOF
{
"values": [
"$1:load"
]
}
EOF
}
# Function to generate the tick.json file content with the given namespace.
function generateTickTag() {
cat << EOF
{
"values": [
"$1:main"
]
}
EOF
}
# Function to generate the content of the load.mcfunction file.
function generateLoadFunction() {
cat << EOF
# Write here, what your pack should do, when loaded:
EOF
}
# Function to generate the content of the main.mcfunction file.
function generateMainFunction() {
cat << EOF
# Write here, what your pack should do in each tick:
EOF
}
# Function to sanitize the name and convert it to a valid namespace
function sanitizeNamespace() {
local input="$1"
local sanitized="${input,,}" # Convert to lowercase
sanitized="${sanitized// /_}" # Replace spaces with underscores
echo "$sanitized"
}
# Function to sanitize the input for a valid file path descriptor.
function sanitizePathDescriptor() {
local input="$1"
local sanitized="${input//\\/}" # Remove backslashes (escape characters)
sanitized="${sanitized//[^[:alnum:]_\-\/. ]/}" # Remove non-alphanumeric, non-underscore, non-hyphen, non-slash, non-space, and non-dot characters
sanitized="${sanitized//\/\//\/}" # Replace double slashes with a single slash
echo "$sanitized"
}
# Function to sanitize the input for a valid directory name.
function sanitizeDirectoryname() {
local input="$1"
local sanitized="${input//\\/}" # Remove backslashes (escape characters)
sanitized="${sanitized//[^[:alnum:]_\- ]/}" # Remove non-alphanumeric, non-underscore, non-space, and non-hyphen characters
echo "$sanitized"
}
# Function to sanitize the input for safe insertion into a JSON file
function sanitizeDescription() {
local input="$1"
local sanitized
sanitized=$(printf '%s' "$input" | sed 's/\\/\\\\/g; s/"/\\"/g')
sanitized="${sanitized//$'\n'/\\n}"
echo "$sanitized"
}
function formatList {
local title="$1"
shift
local array=("$@")
local result="$title:\n"
for element in "${array[@]}"; do
result+="\t- $element\n"
done
echo -e "$result"
}
# Function to gather user input for the datapack details.
# Sets the following variables globally: parent_directory, name, pack_format, description
# The first argument is expected to be a string containing the descriptors of each input to be gathered (or none).
function gatherUserInput() {
# Look at next echo command message for details.
for to_gather in "$@"; do
if [[ "$to_gather" == "Parent directory" ]]; then
while true; do
echo 'Please enter the parent directory to place the project directory in. Leave blank to place it in the current working directory or, if this script is located inside a directory called "scripts", its parent.'
if "$use_gum"; then
parent_dir=$(gum input --placeholder "path/to/parent")
else
read -r parent_dir
fi
if [[ "$parent_dir" == "" || -z "$parent_dir" ]]; then
# No explicit parent path means to choose the directory the script is located in.
parent_dir=$(dirname "$0")
# If the directory of the script is called `scripts`, assume the script to be in a multi-project repo.
if [[ "$parent_dir" == *"/scripts" ]]; then
parent_dir=$(dirname "$parent_dir")
fi
break
elif [[ "$parent_dir" =~ [^\0]+ ]]; then
# An explicit path was given and it's valid under a UNIX-like system.
break
fi
echo "Invalid path!"
done
parent_dir=$(readlink -f "$parent_dir")
fi
# The name must be a valid name for a directory, because well... It's also the name for the directory housing the datapack-files.
if [[ $to_gather == "Name" ]]; then
echo "Please enter the Name of the new Datapack. (Note, that it must also be a valid directory name!)"
if $use_gum; then
name="$(gum input --placeholder "A valid directory name")"
else
read -r name
fi
while true; do
name=$(sanitizeDirectoryname "$name")
if [ -d "$parent_dir/$name" ]; then
echo "This already exists!"
if $use_gum; then
name="$(gum input --prompt "Please enter a different name or cancel: " --placeholder "A valid directory name")"
else
read -r name
fi
elif [ -z "$name" ]; then
echo "Invalid name!"
if $use_gum; then
name="$(gum input --prompt "Please enter a valid name or cancel: " --placeholder "A valid directory name")"
else
read -r name
fi
else
break
fi
done
fi
if [[ $to_gather == "Format" ]]; then
echo "Please enter the pack format. (You can consult https://minecraft.wiki/Pack_format for an up to date reference.)"
if $use_gum; then
pack_format="$(gum input --placeholder "A valid pack format number")"
else
read -r pack_format
fi
while true; do
if [[ "$pack_format" =~ ^[0-9]+$ ]]; then
if [[ ! "$pack_format" -lt 49 ]]; then # Should this script be available in a dedicated repository one day, the upper bound will be updated appropriately.
echo -e "Warning: The pack format is suspiciously high.\nThis warning can be ignored, when 48 is outdated as the highest pack format number."
fi
break
else
echo "Invalid Format!"
if $use_gum; then
pack_format="$(gum input --prompt "Please enter a valid pack format number: " --placeholder "A valid pack format number")"
else
read -r -p "Please enter a valid pack format number: " pack_format
fi
fi
done
fi
if [[ "$to_gather" == "Description" ]]; then
if $use_gum; then
echo "How would you describe the pack?"
description="$(gum write --placeholder "Description of the pack")"
else
local description_lines=()
echo "How would you describe the pack?"
echo "Press \`<ctrl>+D\` on a new line to confirm."
local line
while read -r line; do
description_lines+=("$line")
done
description=$(joinArrayWith '\n' "${description_lines[@]}")
echo "$description"
fi
description=$(sanitizeDescription "$description")
fi
if [[ $to_gather == "Features" ]]; then
# Sets selected_features
gatherPackFeatures
fi
done
}
# Function to gather user input and confirm before generating the datapack.
function gatherAndConfirm() {
local input_confirm
local required_input=("Parent directory" "Name" "Format" "Description" "Features")
while true; do
gatherUserInput "${required_input[@]}"
required_input=()
echo "You're about to create a new pack with the following attributes:"
echo -e "Parent directory: $parent_dir\nName: $name\nPack format: $pack_format\nDescription: $description\n$(formatList "Selected features" "${selected_features[@]}")"
if $use_gum; then
gum confirm "Is this correct?" && break || input_confirm="n"
else
echo "Is this correct? (y|n)"
read -r input_confirm
fi
while true; do
input_confirm="${input_confirm,,}"
if [[ $input_confirm =~ ^[yn]$ ]]; then
break
else
# Can only be reached, if gum is not installed.
echo 'Please answer with "y" or "n"!'
read -r input_confirm
fi
done
if [[ "$input_confirm" == "y" ]]; then
break
elif [[ "$input_confirm" == "n" ]]; then
if $use_gum; then
echo 'What information would you like to edit? Press "x" to select an item or "a" to select all.'
local selected
selected=$(gum choose --no-limit "Parent directory" "Name" "Format" "Description" "Features")
IFS=$'\n' read -r -d '' -a required_input <<< "$selected"$'\n'
else
echo "What information would you like to edit? Answer with a line separated set of: \"Parent directory\", \"Name\", \"Format\" and \"Description\"; Confirm with \`<Ctrl>+D\`."
local line
while read -r line; do
required_input+=("$line")
done
fi
fi
done
}
function setSubDirsNew() {
subdirectories=()
for feature in "${selected_features[@]}"; do
case $feature in
"Functions")
subdirectories+=("function")
;;
"Structures")
subdirectories+=("structure")
;;
"Tags")
subdirectories+=("tags")
;;
"Advancements")
subdirectories+=("advancement")
;;
"Item modifier")
subdirectories+=("item_modifier")
;;
"Loot tables")
subdirectories+=("loot_table")
;;
"Predicates")
subdirectories+=("predicate")
;;
"Recipes")
subdirectories+=("recipe")
;;
"Dimensions")
subdirectories+=("dimension")
;;
"Dimension types")
subdirectories+=("dimension_type")
;;
"Trim materials")
subdirectories+=("trim_material")
;;
"Trim patterns")
subdirectories+=("trim_pattern")
;;
"Chat types")
subdirectories+=("chat_type")
;;
"Damage types")
subdirectories+=("damage_type")
;;
"Banner patterns")
subdirectories+=("banner_pattern")
;;
"Wolf variants")
subdirectories+=("wolf_variant")
;;
"Enchantments")
subdirectories+=("enchantment")
;;
"Enchantment providers")
subdirectories+=("enchantment_provider")
;;
"Paintings")
subdirectories+=("painting_variant")
;;
"Customized worldgeneration")
subdirectories+=("worldgen/biome" "worldgen/configured_carver" "worldgen/configured_feature" "worldgen/density_function" "worldgen/noise" "worldgen/noise_settings" "worldgen/placed_feature" "worldgen/processor_list" "worldgen/structure" "worldgen/structure_set" "worldgen/template_pool" "worldgen/world_preset" "worldgen/flat_level_generator_preset" "worldgen/multi_noise_biome_noise_parameter_list")
;;
esac
done
}
function setSubDirsOld() {
subdirectories=()
for feature in "${selected_features[@]}"; do
case $feature in
"Functions")
subdirectories+=("functions")
;;
"Structures")
subdirectories+=("structures")
;;
"Tags")
subdirectories+=("tags")
;;
"Advancements")
subdirectories+=("advancements")
;;
"Item modifier")
subdirectories+=("item_modifiers")
;;
"Loot tables")
subdirectories+=("loot_tables")
;;
"Predicates")
subdirectories+=("predicates")
;;
"Recipes")
subdirectories+=("recipes")
;;
"Dimensions")
subdirectories+=("dimension")
;;
"Dimension types")
subdirectories+=("dimension_type")
;;
"Trim materials")
subdirectories+=("trim_material")
;;
"Trim patterns")
subdirectories+=("trim_pattern")
;;
"Chat types")
subdirectories+=("chat_type")
;;
"Damage types")
subdirectories+=("damage_type")
;;
"Banner patterns")
subdirectories+=("banner_pattern")
;;
"Wolf variants")
subdirectories+=("wolf_variant")
;;
"Enchantments")
subdirectories+=("enchantment")
;;
"Enchantment providers")
subdirectories+=("enchantment_provider")
;;
"Paintings")
subdirectories+=("painting_variant")
;;
"Customized worldgeneration")
subdirectories+=("worldgen/biome" "worldgen/configured_carver" "worldgen/configured_feature" "worldgen/density_function" "worldgen/noise" "worldgen/noise_settings" "worldgen/placed_feature" "worldgen/processor_list" "worldgen/structure" "worldgen/structure_set" "worldgen/template_pool" "worldgen/world_preset" "worldgen/flat_level_generator_preset" "worldgen/multi_noise_biome_noise_parameter_list")
;;
esac
done
}
# Function to generate the datapack based on user input.
function generate() {
# Sets the following variables globally: namespace
namespace=$(sanitizeNamespace "$name")
echo "Generating new datapack project in $parent_dir..."
mkdir -p "$parent_dir"
cd "$parent_dir" || handleError 2
mkdir "$name" || handleError 3
cd "./$name" || handleError 3
# Inside main project directory:
generatePackMeta "$pack_format" "$description" > pack.mcmeta
mkdir -p "data/$namespace"
cd "./data/$namespace" || handleError 4
if [[ "$pack_format" -ge 43 ]]; then
setSubDirsNew
else
setSubDirsOld
fi
for subdirectory in "${subdirectories[@]}"; do
mkdir -p "$subdirectory"
if [[ $subdirectory == "function" ]]; then
cd "function" || handleError 4
generateMainFunction > "main.mcfunction"
generateLoadFunction > "load.mcfunction"
cd "$parent_dir/$name/data" || handleError 4
mkdir -p "minecraft/tags/function"
cd "./minecraft/tags/function" || handleError 4
generateLoadTag "$namespace" > "load.json"
generateTickTag "$namespace" > "tick.json"
cd "$parent_dir/$name/data/$namespace" || handleError 4
elif [[ $subdirectory == "functions" ]]; then
cd "functions" || handleError 4
generateMainFunction > "main.mcfunction"
generateLoadFunction > "load.mcfunction"
cd "$parent_dir/$name/data" || handleError 4
mkdir -p "minecraft/tags/functions"
cd "./minecraft/tags/functions" || handleError 4
generateLoadTag "$namespace" > "load.json"
generateTickTag "$namespace" > "tick.json"
cd "$parent_dir/$name/data/$namespace" || handleError 4
fi
done
}
# Function to clean up any potentially created directories or files before exiting.
function cleanUp() {
echo "Cleaning up..."
local user_home_dir="$HOME" # User's home directory
# Only proceed with removal if the created directory is within the user's home directory to limit potential risks.
if [[ -n "$parent_dir" && -n "$name" && "$parent_dir" == "$user_home_dir"* ]]; then
if [ -d "$parent_dir/$name" ]; then
rm -rf "${parent_dir:?}/$name" # Safely remove the created directory within user's home.
fi
else
echo "Cleanup is only allowed within your home directory or its subdirectories. Operating outside of these, requires you to do manual clean-up."
echo "The generated files (if any are present) should be in $parent_dir."
fi
}
# Function to handle cleanup and exit on script interruption.
function onInterrupt() {
cleanUp
echo "Exiting..."
exit 1
}
# Function to handle errors during the datapack generation process.
function handleError() {
# Something happened!
local error_code="$1"
case "$error_code" in
2)
echo "Error: Invalid parent directory! Please provide a valid path."
;;
3)
echo "Error: Failed to create the datapack directory. Is the parent directory writable?"
;;
4)
echo "Error: Failed to create the necessary files and directories for the datapack."
;;
5)
echo "Error: Failed to create the directory with the namespace \"$namespace\"."
;;
*)
echo "Unknown error occurred with error code: $error_code"
;;
esac
cleanUp
exit "$error_code"
}
function main() {
detectGum
gatherAndConfirm
generate
echo "Done!"
}
# Execution starts here:
trap onInterrupt SIGINT
main
@eingruenesbeb
Copy link
Author

eingruenesbeb commented Jun 24, 2024

Notes:

  • Always make sure you understand what a script does before running it!
  • Best to use with gum
  • Only keep the last echo in the cleanUp function, if you're uncomfortable with rm -rf
  • Not designed for Windows systems, though you may be able to run it under WSL.
  • You have improvements or issues? Comment here!
  • If you like this script, feel free to give it a 🌟. ^^

How to use:

  1. Download the file and preferably put it into your Datapack project folder under ./scripts.
  2. Make the file executable with chmod +x /path/to/gen_empty_pack.sh
  3. Run it with /path/to/gen_empty_pack.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment