summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorYehonal <yehonal.azeroth@gmail.com>2025-09-06 11:22:22 +0200
committerGitHub <noreply@github.com>2025-09-06 11:22:22 +0200
commitd3a6c09b3164d5ad0f05924bdad30f98416e6503 (patch)
treea931e0ffca2782de26c5f791e04dfca566686dc9 /apps
parent725b475dd4522b34b2182c9721671f1a49ca1c80 (diff)
feat(config): add support for excluding modules during installation and updates (#22793)
Diffstat (limited to 'apps')
-rw-r--r--apps/installer/includes/modules-manager/README.md145
-rw-r--r--apps/installer/includes/modules-manager/modules.sh308
-rwxr-xr-xapps/installer/test/test_module_commands.bats398
3 files changed, 775 insertions, 76 deletions
diff --git a/apps/installer/includes/modules-manager/README.md b/apps/installer/includes/modules-manager/README.md
index f478035beb..93496a91a6 100644
--- a/apps/installer/includes/modules-manager/README.md
+++ b/apps/installer/includes/modules-manager/README.md
@@ -9,6 +9,10 @@ This directory contains the module management system for AzerothCore, providing
- **Custom Directory Naming**: Prevent conflicts with custom directory names
- **Duplicate Prevention**: Smart detection and prevention of duplicate installations
- **Multi-Host Support**: GitHub, GitLab, and other Git hosts
+- **Module Exclusion**: Support for excluding modules via environment variable
+- **Interactive Menu System**: Easy-to-use menu interface for module management
+- **Colored Output**: Enhanced terminal output with color support (respects NO_COLOR)
+- **Flat Directory Structure**: Uses flat module installation (no owner subfolders)
## 📁 File Structure
@@ -100,10 +104,33 @@ repo[:dirname][@branch[:commit]]
# Search with multiple terms
./acore.sh module search auction house
-# Show all available modules
+# Search with input prompt
./acore.sh module search
```
+### Listing Installed Modules
+
+```bash
+# List all installed modules
+./acore.sh module list
+```
+
+### Interactive Menu
+
+```bash
+# Start interactive menu system
+./acore.sh module
+
+# Menu options:
+# s - Search for available modules
+# i - Install one or more modules
+# u - Update installed modules
+# r - Remove installed modules
+# l - List installed modules
+# h - Show detailed help
+# q - Close this menu
+```
+
## 🔍 Cross-Format Recognition
The system intelligently recognizes the same module across different specification formats:
@@ -129,13 +156,55 @@ The system prevents common conflicts:
```bash
# If 'mod-transmog' directory already exists:
$ ./acore.sh module install mod-transmog:mod-transmog
-Error: Directory 'mod-transmog' already exists.
Possible solutions:
1. Use a different directory name: mod-transmog:my-custom-name
2. Remove the existing directory first
3. Use the update command if this is the same module
```
+### Duplicate Module Prevention
+The system uses intelligent owner/name matching to prevent installing the same module multiple times, even when specified in different formats.
+
+## 🚫 Module Exclusion
+
+You can exclude modules from installation using the `MODULES_EXCLUDE_LIST` environment variable:
+
+```bash
+# Exclude specific modules (space-separated)
+export MODULES_EXCLUDE_LIST="mod-test-module azerothcore/mod-dev-only"
+./acore.sh module install --all # Will skip excluded modules
+
+# Supports cross-format matching
+export MODULES_EXCLUDE_LIST="https://github.com/azerothcore/mod-transmog.git"
+./acore.sh module install mod-transmog # Will be skipped as excluded
+```
+
+The exclusion system:
+- Uses the same cross-format recognition as other module operations
+- Works with all installation methods (`install`, `install --all`)
+- Provides clear feedback when modules are skipped
+- Supports URLs, owner/name format, and simple names
+
+## 🎨 Color Support
+
+The module manager provides enhanced terminal output with colors:
+
+- **Info**: Cyan text for informational messages
+- **Success**: Green text for successful operations
+- **Warning**: Yellow text for warnings
+- **Error**: Red text for errors
+- **Headers**: Bold cyan text for section headers
+
+Color support is automatically disabled when:
+- Output is not to a terminal (piped/redirected)
+- `NO_COLOR` environment variable is set
+- Terminal doesn't support colors
+
+You can force color output with:
+```bash
+export FORCE_COLOR=1
+```
+
## 🔄 Integration
### Including in Scripts
@@ -165,24 +234,78 @@ azerothcore/mod-transmog master abc123def456
https://github.com/custom/mod-custom.git develop def456abc789
mod-eluna:custom-eluna-dir main 789abc123def
```
+
+The list maintains:
+- **Alphabetical ordering** by normalized owner/name for consistency
+- **Original format preservation** of how modules were specified
+- **Automatic deduplication** across different specification formats
+- **Custom directory tracking** when specified
+
## 🔧 Configuration
### Environment Variables
-- `MODULES_LIST_FILE`: Override default modules list path
-- `J_PATH_MODULES`: Modules installation directory
-- `AC_PATH_ROOT`: AzerothCore root path
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `MODULES_LIST_FILE` | Override default modules list path | `$AC_PATH_ROOT/conf/modules.list` |
+| `MODULES_EXCLUDE_LIST` | Space-separated list of modules to exclude | - |
+| `J_PATH_MODULES` | Modules installation directory | `$AC_PATH_ROOT/modules` |
+| `AC_PATH_ROOT` | AzerothCore root path | - |
+| `NO_COLOR` | Disable colored output | - |
+| `FORCE_COLOR` | Force colored output even when not TTY | - |
### Default Paths
-- Modules list: `$AC_PATH_ROOT/conf/modules.list`
+- **Modules list**: `$AC_PATH_ROOT/conf/modules.list`
+- **Installation directory**: `$J_PATH_MODULES` (flat structure, no owner subfolders)
+
+## 🏗️ Architecture
+
+### Core Functions
+
+| Function | Purpose |
+|----------|---------|
+| `inst_module()` | Main dispatcher and interactive menu |
+| `inst_parse_module_spec()` | Parse advanced module syntax |
+| `inst_extract_owner_name()` | Normalize modules for cross-format recognition |
+| `inst_mod_list_*()` | Module list management (read/write/update) |
+| `inst_module_*()` | Module operations (install/update/remove/search) |
+
+### Key Features
+
+- **Flat Directory Structure**: All modules install directly under `modules/` without owner subdirectories
+- **Smart Conflict Detection**: Prevents directory name conflicts with helpful suggestions
+- **Cross-Platform Compatibility**: Works on Linux, macOS, and Windows (Git Bash)
+- **Version Compatibility**: Checks `acore-module.json` for AzerothCore version compatibility
+- **Git Integration**: Uses Joiner system for Git repository management
+
+### Debug Mode
+
+For debugging module operations, you can examine the generated commands:
+```bash
+# Check what Joiner commands would be executed
+tail -f /tmp/joiner_called.txt # In test environments
+```
## 🤝 Contributing
When modifying the module manager:
-1. Maintain backwards compatibility
-2. Update tests in `test_module_commands.bats`
-3. Update this documentation
-4. Test cross-format recognition thoroughly
-5. Ensure helpful error messages
+1. **Maintain backwards compatibility** with existing module list format
+2. **Update tests** in `test_module_commands.bats` for new functionality
+3. **Update this documentation** for any new features or changes
+4. **Test cross-format recognition** thoroughly across all supported formats
+5. **Ensure helpful error messages** for common user mistakes
+6. **Test exclusion functionality** with various module specification formats
+7. **Verify color output** works correctly in different terminal environments
+
+### Testing Guidelines
+```bash
+# Run all module-related tests
+cd apps/installer
+bats test/test_module_commands.bats
+# Test with different environments
+NO_COLOR=1 ./acore.sh module list
+FORCE_COLOR=1 ./acore.sh module help
+```
diff --git a/apps/installer/includes/modules-manager/modules.sh b/apps/installer/includes/modules-manager/modules.sh
index 88cadf01e2..787d07677c 100644
--- a/apps/installer/includes/modules-manager/modules.sh
+++ b/apps/installer/includes/modules-manager/modules.sh
@@ -28,6 +28,49 @@ source "$CURRENT_PATH/../../../bash_shared/includes.sh"
source "$CURRENT_PATH/../includes.sh"
source "$AC_PATH_APPS/bash_shared/menu_system.sh"
+# -----------------------------------------------------------------------------
+# Color support (disabled when not a TTY or NO_COLOR is set)
+# -----------------------------------------------------------------------------
+if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
+ if command -v tput >/dev/null 2>&1; then
+ _ac_cols=$(tput colors 2>/dev/null || echo 0)
+ else
+ _ac_cols=0
+ fi
+else
+ _ac_cols=0
+fi
+
+if [ "${FORCE_COLOR:-}" != "" ] || [ "${_ac_cols}" -ge 8 ]; then
+ C_RESET='\033[0m'
+ C_BOLD='\033[1m'
+ C_DIM='\033[2m'
+ C_RED='\033[31m'
+ C_GREEN='\033[32m'
+ C_YELLOW='\033[33m'
+ C_BLUE='\033[34m'
+ C_MAGENTA='\033[35m'
+ C_CYAN='\033[36m'
+else
+ C_RESET=''
+ C_BOLD=''
+ C_DIM=''
+ C_RED=''
+ C_GREEN=''
+ C_YELLOW=''
+ C_BLUE=''
+ C_MAGENTA=''
+ C_CYAN=''
+fi
+
+# Simple helpers for consistent colored output
+function print_info() { printf "%b\n" "${C_CYAN}$*${C_RESET}"; }
+function print_warn() { printf "%b\n" "${C_YELLOW}$*${C_RESET}"; }
+function print_error() { printf "%b\n" "${C_RED}$*${C_RESET}"; }
+function print_success() { printf "%b\n" "${C_GREEN}$*${C_RESET}"; }
+function print_skip() { printf "%b\n" "${C_BLUE}$*${C_RESET}"; }
+function print_header() { printf "%b\n" "${C_BOLD}${C_CYAN}$*${C_RESET}"; }
+
# Module management menu definition
# Format: "key|short|description"
module_menu_items=(
@@ -65,11 +108,11 @@ function handle_module_command() {
inst_module_help
;;
"quit")
- echo "Exiting module manager..."
+ print_info "Exiting module manager..."
return 0
;;
*)
- echo "Invalid option. Use 'help' to see available commands."
+ print_error "Invalid option. Use 'help' to see available commands."
return 1
;;
esac
@@ -77,7 +120,7 @@ function handle_module_command() {
# Show detailed module help
function inst_module_help() {
- echo "AzerothCore Module Manager Help"
+ print_header "AzerothCore Module Manager Help"
echo "==============================="
echo ""
echo "Usage:"
@@ -106,20 +149,20 @@ function inst_module_help() {
# List installed modules
function inst_module_list() {
- echo "Installed Modules:"
+ print_header "Installed Modules"
echo "=================="
local count=0
while read -r repo_ref branch commit; do
[[ -z "$repo_ref" ]] && continue
count=$((count + 1))
- echo " $count. $repo_ref ($branch)"
+ printf " %s. %b (%s)%b\n" "$count" "${C_GREEN}${repo_ref}" "${branch}" "${C_RESET}"
if [[ "$commit" != "-" ]]; then
- echo " Commit: $commit"
+ printf " %bCommit:%b %s\n" "${C_DIM}" "${C_RESET}" "$commit"
fi
done < <(inst_mod_list_read)
if [[ $count -eq 0 ]]; then
- echo " No modules installed."
+ print_warn " No modules installed."
fi
echo ""
}
@@ -160,8 +203,7 @@ function inst_module() {
inst_module_list "${args[@]}"
;;
*)
- echo "Unknown module command: $cmd"
- echo "Use 'help' to see available commands."
+ print_error "Unknown module command: $cmd. Use 'help' to see available commands."
return 1
;;
esac
@@ -188,20 +230,72 @@ function inst_parse_module_spec() {
# Parse the new syntax: repo[:dirname][@branch[:commit]]
- # First, extract custom directory name if present (format: repo:dirname@branch)
+ # First, check if this is a URL (contains :// or starts with git@)
+ local is_url=0
+ if [[ "$spec" =~ :// ]] || [[ "$spec" =~ ^git@ ]]; then
+ is_url=1
+ fi
+
+ # Parse directory and branch differently for URLs vs simple names
local repo_with_branch="$spec"
- if [[ "$spec" =~ ^([^@:]+):([^@:]+)(@.*)?$ ]]; then
- repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}"
- dirname="${BASH_REMATCH[2]}"
+ if [[ $is_url -eq 1 ]]; then
+ # For URLs, look for :dirname pattern, but be careful about ports
+ # Strategy: only match :dirname if it's clearly after the repository path
+
+ # Look for :dirname patterns at the end, but not if it looks like a port
+ if [[ "$spec" =~ ^(.*\.git):([^@/:]+)(@.*)?$ ]]; then
+ # Repo ending with .git:dirname
+ repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}"
+ dirname="${BASH_REMATCH[2]}"
+ elif [[ "$spec" =~ ^(.*://[^/]+/[^:]*[^0-9]):([^@/:]+)(@.*)?$ ]]; then
+ # URL with path ending in non-digit:dirname (avoid matching ports)
+ repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}"
+ dirname="${BASH_REMATCH[2]}"
+ fi
+ # If no custom dirname found, repo_with_branch remains the original spec
+ else
+ # For simple names, use the original logic
+ if [[ "$spec" =~ ^([^@:]+):([^@:]+)(@.*)?$ ]]; then
+ repo_with_branch="${BASH_REMATCH[1]}${BASH_REMATCH[3]}"
+ dirname="${BASH_REMATCH[2]}"
+ fi
fi
# Now parse branch and commit from the repo part
- if [[ "$repo_with_branch" =~ ^([^@]+)@([^:]+)(:(.+))?$ ]]; then
- repo_part="${BASH_REMATCH[1]}"
- branch="${BASH_REMATCH[2]}"
- commit="${BASH_REMATCH[4]:-}"
+ # Be careful not to confuse URL @ with branch @
+ if [[ "$repo_with_branch" =~ :// ]]; then
+ # For URLs, look for @ after the authority part
+ if [[ "$repo_with_branch" =~ ^[^/]*//[^/]+/.*@([^:]+)(:(.+))?$ ]]; then
+ # @ found in path part - treat as branch
+ repo_part="${repo_with_branch%@*}"
+ branch="${BASH_REMATCH[1]}"
+ commit="${BASH_REMATCH[3]:-}"
+ elif [[ "$repo_with_branch" =~ ^([^@]*@[^/]+/.*)@([^:]+)(:(.+))?$ ]]; then
+ # @ found after URL authority @ - treat as branch
+ repo_part="${BASH_REMATCH[1]}"
+ branch="${BASH_REMATCH[2]}"
+ commit="${BASH_REMATCH[4]:-}"
+ else
+ repo_part="$repo_with_branch"
+ fi
+ elif [[ "$repo_with_branch" =~ ^git@ ]]; then
+ # Git SSH format - look for @ after the initial git@host: part
+ if [[ "$repo_with_branch" =~ ^git@[^:]+:.*@([^:]+)(:(.+))?$ ]]; then
+ repo_part="${repo_with_branch%@*}"
+ branch="${BASH_REMATCH[1]}"
+ commit="${BASH_REMATCH[3]:-}"
+ else
+ repo_part="$repo_with_branch"
+ fi
else
- repo_part="$repo_with_branch"
+ # Non-URL format - use original logic
+ if [[ "$repo_with_branch" =~ ^([^@]+)@([^:]+)(:(.+))?$ ]]; then
+ repo_part="${BASH_REMATCH[1]}"
+ branch="${BASH_REMATCH[2]}"
+ commit="${BASH_REMATCH[4]:-}"
+ else
+ repo_part="$repo_with_branch"
+ fi
fi
# Normalize repo reference and extract owner/name.
@@ -210,10 +304,39 @@ function inst_parse_module_spec() {
# If repo_ref is a URL, extract owner/name from path when possible
if [[ "$repo_ref" =~ :// ]] || [[ "$repo_ref" =~ ^git@ ]]; then
- # Extract owner/name (last two path components)
- owner_repo=$(echo "$repo_ref" | sed -E 's#(git@[^:]+:|https?://[^/]+/|ssh://[^/]+/)?(.*?)(\.git)?$#\2#')
- owner="$(echo "$owner_repo" | awk -F'/' '{print $(NF-1)}')"
- name="$(echo "$owner_repo" | awk -F'/' '{print $NF}' | sed -E 's/\.git$//')"
+ # Handle various URL formats
+ local path_part=""
+ if [[ "$repo_ref" =~ ^https?://[^/]+:?[0-9]*/(.+)$ ]]; then
+ # HTTPS URL (with or without port)
+ path_part="${BASH_REMATCH[1]}"
+ elif [[ "$repo_ref" =~ ^ssh://.*@[^/]+:?[0-9]*/(.+)$ ]]; then
+ # SSH URL with user@host:port/path format
+ path_part="${BASH_REMATCH[1]}"
+ elif [[ "$repo_ref" =~ ^ssh://[^@/]+:?[0-9]*/(.+)$ ]]; then
+ # SSH URL with host:port/path format (no user@)
+ path_part="${BASH_REMATCH[1]}"
+ elif [[ "$repo_ref" =~ ^git@[^:]+:(.+)$ ]]; then
+ # Git SSH format (git@host:path)
+ path_part="${BASH_REMATCH[1]}"
+ fi
+
+ # Extract owner/name from path
+ if [[ -n "$path_part" ]]; then
+ # Remove .git suffix and any :dirname suffix
+ path_part="${path_part%.git}"
+ path_part="${path_part%:*}"
+
+ if [[ "$path_part" == *"/"* ]]; then
+ owner="$(echo "$path_part" | awk -F'/' '{print $(NF-1)}')"
+ name="$(echo "$path_part" | awk -F'/' '{print $NF}')"
+ else
+ owner="unknown"
+ name="$path_part"
+ fi
+ else
+ owner="unknown"
+ name="unknown"
+ fi
else
owner_repo="$repo_ref"
if [[ "$owner_repo" == *"/"* ]]; then
@@ -266,21 +389,28 @@ function inst_extract_owner_name {
base_ref="${repo_ref%%:*}"
fi
- if [[ "$base_ref" =~ ^https?://github\.com/([^/]+)/([^/]+)(\.git)?(/.*)?$ ]]; then
- # HTTPS URL format - check this first before owner/name pattern
+ # Handle various URL formats with possible ports
+ if [[ "$base_ref" =~ ^https?://[^/]+:?[0-9]*/([^/]+)/([^/?]+) ]]; then
+ # HTTPS URL format (with or without port) - matches github.com, gitlab.com, custom hosts
+ local owner="${BASH_REMATCH[1]}"
local name="${BASH_REMATCH[2]}"
- name="${name%.git}" # Remove .git suffix if present
- echo "${BASH_REMATCH[1]}/$name"
- elif [[ "$base_ref" =~ ^https?://gitlab\.com/([^/]+)/([^/]+)(\.git)?(/.*)?$ ]]; then
- # GitLab URL format
+ name="${name%:*}" # Remove any :dirname suffix first
+ name="${name%.git}" # Then remove .git suffix if present
+ echo "$owner/$name"
+ elif [[ "$base_ref" =~ ^ssh://[^/]+:?[0-9]*/([^/]+)/([^/?]+) ]]; then
+ # SSH URL format (with or without port)
+ local owner="${BASH_REMATCH[1]}"
local name="${BASH_REMATCH[2]}"
- name="${name%.git}" # Remove .git suffix if present
- echo "${BASH_REMATCH[1]}/$name"
- elif [[ "$base_ref" =~ ^git@github\.com:([^/]+)/([^/]+)(\.git)?$ ]]; then
- # SSH URL format
+ name="${name%:*}" # Remove any :dirname suffix first
+ name="${name%.git}" # Then remove .git suffix if present
+ echo "$owner/$name"
+ elif [[ "$base_ref" =~ ^git@[^:]+:([^/]+)/([^/?]+) ]]; then
+ # Git SSH format (git@host:owner/repo)
+ local owner="${BASH_REMATCH[1]}"
local name="${BASH_REMATCH[2]}"
- name="${name%.git}" # Remove .git suffix if present
- echo "${BASH_REMATCH[1]}/$name"
+ name="${name%:*}" # Remove any :dirname suffix first
+ name="${name%.git}" # Then remove .git suffix if present
+ echo "$owner/$name"
elif [[ "$base_ref" =~ ^[^/]+/[^/]+$ ]]; then
# Format: owner/name (check after URL patterns)
echo "$base_ref"
@@ -330,6 +460,40 @@ function inst_mod_list_read() {
done < "$file"
}
+# Check whether a module spec matches the exclusion list.
+# - Reads space/newline separated items from env var MODULES_EXCLUDE_LIST
+# - Supports cross-format matching via inst_extract_owner_name
+# Returns 0 if excluded, 1 otherwise.
+function inst_mod_is_excluded() {
+ local spec="$1"
+ local target_owner_name
+ target_owner_name=$(inst_extract_owner_name "$spec")
+
+ # No exclusions configured
+ if [[ -z "${MODULES_EXCLUDE_LIST:-}" ]]; then
+ return 1
+ fi
+
+ # Split on default IFS (space, tab, newline)
+ local items=()
+ # Use mapfile to split MODULES_EXCLUDE_LIST on newlines; fallback to space if no newlines
+ if [[ "${MODULES_EXCLUDE_LIST}" == *$'\n'* ]]; then
+ mapfile -t items <<< "${MODULES_EXCLUDE_LIST}"
+ else
+ read -r -a items <<< "${MODULES_EXCLUDE_LIST}"
+ fi
+
+ local it it_owner
+ for it in "${items[@]}"; do
+ [[ -z "$it" ]] && continue
+ it_owner=$(inst_extract_owner_name "$it")
+ if [[ "$it_owner" == "$target_owner_name" ]]; then
+ return 0
+ fi
+ done
+ return 1
+}
+
# Add or update an entry in the list: repo_ref branch commit
# Removes any existing entries with the same owner/name to avoid duplicates
function inst_mod_list_upsert() {
@@ -448,12 +612,12 @@ function inst_check_module_conflict {
local repo_ref="$2"
if [ -d "$J_PATH_MODULES/$dirname" ]; then
- echo "Error: Directory '$dirname' already exists."
- echo "Possible solutions:"
- echo " 1. Use a different directory name: $repo_ref:my-custom-name"
- echo " 2. Remove the existing directory first"
- echo " 3. Use the update command if this is the same module"
- return 1
+ print_error "Error: Directory '$dirname' already exists."
+ print_warn "Possible solutions:"
+ echo " 1. Use a different directory name: $repo_ref:my-custom-name"
+ echo " 2. Remove the existing directory first"
+ echo " 3. Use the update command if this is the same module"
+ return 1
fi
return 0
}
@@ -511,7 +675,7 @@ function inst_module_search {
local CATALOG_URL="https://www.azerothcore.org/data/catalogue.json"
- echo "Searching ${terms[*]}..."
+ print_header "Searching ${terms[*]}..."
echo ""
# Build candidate list from catalogue (full_name = owner/repo)
@@ -567,9 +731,9 @@ function inst_module_search {
read v b < <(inst_getVersionBranch "https://raw.githubusercontent.com/${mod_full}/master/acore-module.json")
if [[ "$b" != "none" ]]; then
- echo "-> $mod (tested with AC version: $v)"
+ printf "%b -> %b (tested with AC version: %s)%b\n" "" "${C_GREEN}${mod}${C_RESET}" "$v" ""
else
- echo "-> $mod (NOTE: The module latest tested AC revision is Unknown)"
+ printf "%b -> %b %b(NOTE: The module latest tested AC revision is Unknown)%b\n" "" "${C_GREEN}${mod}${C_RESET}" "${C_YELLOW}" "${C_RESET}"
fi
done
@@ -590,7 +754,7 @@ function inst_module_install {
local modules=("$@")
- echo "Installing modules: ${modules[*]}"
+ print_header "Installing modules: ${modules[*]}"
if $use_all; then
# Install all modules from the list (respecting recorded branch and commit).
@@ -602,14 +766,18 @@ function inst_module_install {
local dup_error=0
while read -r repo_ref branch commit; do
[ -z "$repo_ref" ] && continue
+ # Skip excluded modules when checking duplicates
+ if inst_mod_is_excluded "$repo_ref"; then
+ continue
+ fi
parsed_output=$(inst_parse_module_spec "$repo_ref")
IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output"
# dirname defaults to repo name; flat install path uses dirname only
if [[ -n "${_seen[$dirname]:-}" ]]; then
- echo "Error: duplicate module target directory '$dirname' detected in modules.list:"
+ print_error "Error: duplicate module target directory '$dirname' detected in modules.list:"
echo " - ${_first[$dirname]}"
echo " - ${repo_ref}"
- echo "Use a custom folder name to disambiguate, e.g.: ${repo_ref}:$dirname-alt"
+ print_warn "Use a custom folder name to disambiguate, e.g.: ${repo_ref}:$dirname-alt"
dup_error=1
else
_seen[$dirname]=1
@@ -623,11 +791,16 @@ function inst_module_install {
# Second pass: install in flat modules directory (no owner subfolders)
while read -r repo_ref branch commit; do
[ -z "$repo_ref" ] && continue
+ # Skip excluded entries during installation
+ if inst_mod_is_excluded "$repo_ref"; then
+ print_warn "[$repo_ref] Excluded by MODULES_EXCLUDE_LIST (skipping)."
+ continue
+ fi
parsed_output=$(inst_parse_module_spec "$repo_ref")
IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output"
if [ -d "$J_PATH_MODULES/$dirname" ]; then
- echo "[$repo_ref] Already installed (skipping)."
+ print_skip "[$repo_ref] Already installed (skipping)."
continue
fi
@@ -642,9 +815,9 @@ function inst_module_install {
local curCommit
curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "")
inst_mod_list_upsert "$repo_ref" "$branch" "$curCommit"
- echo "[$repo_ref] Installed."
+ print_success "[$repo_ref] Installed."
else
- echo "[$repo_ref] Install failed."
+ print_error "[$repo_ref] Install failed."
exit 1;
fi
done < <(inst_mod_list_read)
@@ -663,7 +836,7 @@ function inst_module_install {
# Check if module is already installed (by owner/name matching)
existing_repo_ref=$(inst_mod_is_installed "$spec" || true)
if [ -n "$existing_repo_ref" ]; then
- echo "[$spec] Already installed as [$existing_repo_ref] (skipping)."
+ print_skip "[$spec] Already installed as [$existing_repo_ref] (skipping)."
continue
fi
@@ -689,14 +862,14 @@ function inst_module_install {
fi
if [[ "$v" == "none" || "$v" == "not-defined" || "$b" == "none" ]]; then
def="$(inst_get_default_branch "$repo_ref")"
- echo "Warning: $repo_ref has no compatible acore-module.json; installing from branch '$def' (latest commit)."
+ print_warn "Warning: $repo_ref has no compatible acore-module.json; installing from branch '$def' (latest commit)."
b="$def"
fi
fi
# Use flat directory structure with custom directory name
if [ -d "$J_PATH_MODULES/$dirname" ]; then
- echo "[$repo_ref] Already installed (skipping)."
+ print_skip "[$repo_ref] Already installed (skipping)."
curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "")
inst_mod_list_upsert "$repo_ref" "$b" "$curCommit"
continue
@@ -709,14 +882,14 @@ function inst_module_install {
if git -C "$J_PATH_MODULES/$dirname" rev-parse --verify "$override_commit" >/dev/null 2>&1; then
git -C "$J_PATH_MODULES/$dirname" checkout --quiet "$override_commit"
else
- echo "[$repo_ref] Warning: provided commit '$override_commit' not found; staying on branch '$b' HEAD."
+ print_warn "[$repo_ref] provided commit '$override_commit' not found; staying on branch '$b' HEAD."
fi
fi
curCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "")
inst_mod_list_upsert "$repo_ref" "$b" "$curCommit"
- echo "[$repo_ref] Installed in '$dirname'. Please re-run compiling and db assembly."
+ print_success "[$repo_ref] Installed in '$dirname'. Please re-run compiling and db assembly."
else
- echo "[$repo_ref] Install failed or module not found"
+ print_error "[$repo_ref] Install failed or module not found"
exit 1;
fi
done
@@ -748,21 +921,26 @@ function inst_module_update {
local line repo_ref branch commit newCommit owner modname url dirname
while read -r repo_ref branch commit; do
[ -z "$repo_ref" ] && continue
+ # Skip excluded modules during update --all
+ if inst_mod_is_excluded "$repo_ref"; then
+ print_warn "[$repo_ref] Excluded by MODULES_EXCLUDE_LIST (skipping)."
+ continue
+ fi
parsed_output=$(inst_parse_module_spec "$repo_ref")
IFS=' ' read -r _ owner modname _ _ url dirname <<< "$parsed_output"
dirname="${dirname:-$modname}"
if [ ! -d "$J_PATH_MODULES/$dirname/" ]; then
- echo "[$repo_ref] Not installed locally, skipping."
+ print_skip "[$repo_ref] Not installed locally, skipping."
continue
fi
if Joiner:upd_repo "$url" "$dirname" "$branch" ""; then
newCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "")
inst_mod_list_upsert "$repo_ref" "$branch" "$newCommit"
- echo "[$repo_ref] Updated to latest commit on '$branch'."
+ print_success "[$repo_ref] Updated to latest commit on '$branch'."
else
- echo "[$repo_ref] Cannot update"
+ print_error "[$repo_ref] Cannot update"
fi
done < <(inst_mod_list_read)
else
@@ -793,11 +971,11 @@ function inst_module_update {
fi
if [[ "$v" == "none" || "$v" == "not-defined" || "$b" == "none" ]]; then
if branch=$(git -C "$J_PATH_MODULES/$dirname" rev-parse --abbrev-ref HEAD 2>/dev/null); then
- echo "Warning: $repo_ref has no compatible acore-module.json; updating current branch '$branch'."
+ print_warn "Warning: $repo_ref has no compatible acore-module.json; updating current branch '$branch'."
b="$branch"
else
def="$(inst_get_default_branch "$repo_ref")"
- echo "Warning: $repo_ref has no compatible acore-module.json and no git branch detected; updating default branch '$def'."
+ print_warn "Warning: $repo_ref has no compatible acore-module.json and no git branch detected; updating default branch '$def'."
b="$def"
fi
fi
@@ -806,12 +984,12 @@ function inst_module_update {
if Joiner:upd_repo "$url" "$dirname" "$b" ""; then
newCommit=$(git -C "$J_PATH_MODULES/$dirname" rev-parse HEAD 2>/dev/null || echo "")
inst_mod_list_upsert "$repo_ref" "$b" "$newCommit"
- echo "[$repo_ref] Done, please re-run compiling and db assembly"
+ print_success "[$repo_ref] Done, please re-run compiling and db assembly"
else
- echo "[$repo_ref] Cannot update"
+ print_error "[$repo_ref] Cannot update"
fi
else
- echo "[$repo_ref] Cannot update! Path doesn't exist ($J_PATH_MODULES/$dirname/)"
+ print_error "[$repo_ref] Cannot update! Path doesn't exist ($J_PATH_MODULES/$dirname/)"
fi
done
fi
@@ -840,9 +1018,9 @@ function inst_module_remove {
dirname="${dirname:-$modname}"
if Joiner:remove "$dirname" ""; then
inst_mod_list_remove "$repo_ref"
- echo "[$repo_ref] Done, please re-run compiling"
+ print_success "[$repo_ref] Done, please re-run compiling"
else
- echo "[$repo_ref] Cannot remove"
+ print_error "[$repo_ref] Cannot remove"
fi
done
diff --git a/apps/installer/test/test_module_commands.bats b/apps/installer/test/test_module_commands.bats
index 30ee94e816..1223a80a6d 100755
--- a/apps/installer/test/test_module_commands.bats
+++ b/apps/installer/test/test_module_commands.bats
@@ -146,6 +146,27 @@ teardown() {
[ "$output" = "myorg/mymodule" ]
}
+@test "inst_extract_owner_name should handle URLs with ports correctly" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test HTTPS URL with port
+ run inst_extract_owner_name "https://example.com:8080/user/repo.git"
+ [ "$output" = "user/repo" ]
+
+ # Test SSH URL with port
+ run inst_extract_owner_name "ssh://git@example.com:2222/owner/module"
+ [ "$output" = "owner/module" ]
+
+ # Test URL with port and custom directory (should ignore the directory part)
+ run inst_extract_owner_name "https://gitlab.internal:9443/team/project.git:custom-dir"
+ [ "$output" = "team/project" ]
+
+ # Test complex URL with port (should extract owner/name correctly)
+ run inst_extract_owner_name "https://git.company.com:8443/department/awesome-module.git"
+ [ "$output" = "department/awesome-module" ]
+}
+
@test "duplicate module entries should be prevented across different formats" {
cd "$TEST_DIR"
source "$TEST_DIR/apps/installer/includes/includes.sh"
@@ -189,6 +210,30 @@ teardown() {
[ "$status" -ne 0 ]
}
+@test "module installed via URL with port should be recognized correctly" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Install via URL with port
+ inst_mod_list_upsert "https://gitlab.internal:9443/myorg/my-module.git" "master" "abc123"
+
+ # Should be detected as installed using normalized owner/name
+ run inst_mod_is_installed "myorg/my-module"
+ [ "$status" -eq 0 ]
+
+ # Should be detected when checking with different URL format
+ run inst_mod_is_installed "ssh://git@gitlab.internal:9443/myorg/my-module"
+ [ "$status" -eq 0 ]
+
+ # Should be detected when checking with custom directory syntax
+ run inst_mod_is_installed "myorg/my-module:custom-dir"
+ [ "$status" -eq 0 ]
+
+ # Different module should not be detected
+ run inst_mod_is_installed "myorg/different-module"
+ [ "$status" -ne 0 ]
+}
+
@test "cross-format module removal should work" {
cd "$TEST_DIR"
source "$TEST_DIR/apps/installer/includes/includes.sh"
@@ -355,3 +400,356 @@ EOF
[ "$owner_name_format" = "azerothcore/mod-transmog" ]
[ "$simple_format" = "azerothcore/mod-transmog" ]
}
+
+# Tests for module exclusion functionality
+
+@test "module exclusion should work with MODULES_EXCLUDE_LIST" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test exclusion with simple name
+ export MODULES_EXCLUDE_LIST="mod-test-module"
+ run inst_mod_is_excluded "mod-test-module"
+ [ "$status" -eq 0 ]
+
+ # Test exclusion with owner/name format
+ export MODULES_EXCLUDE_LIST="azerothcore/mod-test"
+ run inst_mod_is_excluded "mod-test"
+ [ "$status" -eq 0 ]
+
+ # Test exclusion with space-separated list
+ export MODULES_EXCLUDE_LIST="mod-one mod-two mod-three"
+ run inst_mod_is_excluded "mod-two"
+ [ "$status" -eq 0 ]
+
+ # Test exclusion with newline-separated list
+ export MODULES_EXCLUDE_LIST="
+mod-alpha
+mod-beta
+mod-gamma
+"
+ run inst_mod_is_excluded "mod-beta"
+ [ "$status" -eq 0 ]
+
+ # Test exclusion with URL format
+ export MODULES_EXCLUDE_LIST="https://github.com/azerothcore/mod-transmog.git"
+ run inst_mod_is_excluded "mod-transmog"
+ [ "$status" -eq 0 ]
+
+ # Test non-excluded module
+ export MODULES_EXCLUDE_LIST="mod-other"
+ run inst_mod_is_excluded "mod-transmog"
+ [ "$status" -eq 1 ]
+
+ # Test empty exclusion list
+ unset MODULES_EXCLUDE_LIST
+ run inst_mod_is_excluded "mod-transmog"
+ [ "$status" -eq 1 ]
+}
+
+@test "install --all should skip excluded modules" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Setup modules list with excluded module
+ mkdir -p "$TEST_DIR/conf"
+ cat > "$TEST_DIR/conf/modules.list" << 'EOF'
+azerothcore/mod-transmog master abc123
+azerothcore/mod-excluded master def456
+EOF
+
+ # Set exclusion list
+ export MODULES_EXCLUDE_LIST="mod-excluded"
+
+ # Mock the install process to capture output
+ run bash -c "source '$TEST_DIR/apps/installer/includes/includes.sh' && inst_module_install --all 2>&1"
+
+ # Should show that excluded module was skipped
+ [[ "$output" == *"azerothcore/mod-excluded"* && "$output" == *"Excluded by MODULES_EXCLUDE_LIST"* && "$output" == *"skipping"* ]]
+}
+
+@test "exclusion should work with multiple formats in same list" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test multiple exclusion formats
+ export MODULES_EXCLUDE_LIST="mod-test https://github.com/azerothcore/mod-transmog.git custom/mod-other"
+
+ run inst_mod_is_excluded "mod-test"
+ [ "$status" -eq 0 ]
+
+ run inst_mod_is_excluded "azerothcore/mod-transmog"
+ [ "$status" -eq 0 ]
+
+ run inst_mod_is_excluded "custom/mod-other"
+ [ "$status" -eq 0 ]
+
+ run inst_mod_is_excluded "mod-allowed"
+ [ "$status" -eq 1 ]
+}
+
+# Tests for color support functionality
+
+@test "color functions should work correctly" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test that print functions exist and work
+ run print_info "test message"
+ [ "$status" -eq 0 ]
+
+ run print_warn "test warning"
+ [ "$status" -eq 0 ]
+
+ run print_error "test error"
+ [ "$status" -eq 0 ]
+
+ run print_success "test success"
+ [ "$status" -eq 0 ]
+
+ run print_skip "test skip"
+ [ "$status" -eq 0 ]
+
+ run print_header "test header"
+ [ "$status" -eq 0 ]
+}
+
+@test "color support should respect NO_COLOR environment variable" {
+ cd "$TEST_DIR"
+
+ # Test with NO_COLOR set
+ export NO_COLOR=1
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Colors should be empty when NO_COLOR is set
+ [ -z "$C_RED" ]
+ [ -z "$C_GREEN" ]
+ [ -z "$C_RESET" ]
+}
+
+# Tests for interactive menu system
+
+@test "module help should display comprehensive help" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ run inst_module_help
+ [ "$status" -eq 0 ]
+
+ # Should contain key sections
+ [[ "$output" =~ "Module Manager Help" ]]
+ [[ "$output" =~ "Usage:" ]]
+ [[ "$output" =~ "Module Specification Syntax:" ]]
+ [[ "$output" =~ "Examples:" ]]
+}
+
+@test "module list should show installed modules correctly" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Setup modules list
+ mkdir -p "$TEST_DIR/conf"
+ cat > "$TEST_DIR/conf/modules.list" << 'EOF'
+azerothcore/mod-transmog master abc123
+custom/mod-test develop def456
+EOF
+
+ run inst_module_list
+ [ "$status" -eq 0 ]
+
+ # Should show both modules
+ [[ "$output" =~ "mod-transmog" ]]
+ [[ "$output" =~ "custom/mod-test" ]]
+ [[ "$output" =~ "master" ]]
+ [[ "$output" =~ "develop" ]]
+}
+
+@test "module list should handle empty list gracefully" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Ensure empty modules list
+ mkdir -p "$TEST_DIR/conf"
+ touch "$TEST_DIR/conf/modules.list"
+
+ run inst_module_list
+ [ "$status" -eq 0 ]
+ [[ "$output" =~ "No modules installed" ]]
+}
+
+# Tests for advanced parsing edge cases
+
+@test "parsing should handle complex URL formats" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test GitLab URL with custom directory and branch
+ run inst_parse_module_spec "https://gitlab.com/myorg/mymodule.git:custom-dir@develop:abc123"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$repo_ref" = "https://gitlab.com/myorg/mymodule.git" ]
+ [ "$owner" = "myorg" ]
+ [ "$name" = "mymodule" ]
+ [ "$branch" = "develop" ]
+ [ "$commit" = "abc123" ]
+ [ "$dirname" = "custom-dir" ]
+}
+
+@test "parsing should handle URLs with ports correctly (fix for port/dirname confusion)" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test HTTPS URL with port - should NOT treat port as dirname
+ run inst_parse_module_spec "https://example.com:8080/user/repo.git"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$repo_ref" = "https://example.com:8080/user/repo.git" ]
+ [ "$owner" = "user" ]
+ [ "$name" = "repo" ]
+ [ "$branch" = "-" ]
+ [ "$commit" = "-" ]
+ [ "$url" = "https://example.com:8080/user/repo.git" ]
+ [ "$dirname" = "repo" ] # Should default to repo name, NOT port number
+}
+
+@test "parsing should handle URLs with ports and custom directory correctly" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test URL with port AND custom directory - should parse custom directory correctly
+ run inst_parse_module_spec "https://example.com:8080/user/repo.git:custom-dir"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$repo_ref" = "https://example.com:8080/user/repo.git" ]
+ [ "$owner" = "user" ]
+ [ "$name" = "repo" ]
+ [ "$branch" = "-" ]
+ [ "$commit" = "-" ]
+ [ "$url" = "https://example.com:8080/user/repo.git" ]
+ [ "$dirname" = "custom-dir" ] # Should be custom-dir, not port number
+}
+
+@test "parsing should handle SSH URLs with ports correctly" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test SSH URL with port
+ run inst_parse_module_spec "ssh://git@example.com:2222/user/repo"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$repo_ref" = "ssh://git@example.com:2222/user/repo" ]
+ [ "$owner" = "user" ]
+ [ "$name" = "repo" ]
+ [ "$dirname" = "repo" ] # Should be repo name, not port number
+}
+
+@test "parsing should handle SSH URLs with ports and custom directory" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test SSH URL with port and custom directory
+ run inst_parse_module_spec "ssh://git@example.com:2222/user/repo:my-custom-dir@develop"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$repo_ref" = "ssh://git@example.com:2222/user/repo" ]
+ [ "$owner" = "user" ]
+ [ "$name" = "repo" ]
+ [ "$branch" = "develop" ]
+ [ "$dirname" = "my-custom-dir" ]
+}
+
+@test "parsing should handle complex URLs with ports, custom dirs, and branches" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Test comprehensive URL with port, custom directory, branch, and commit
+ run inst_parse_module_spec "https://gitlab.example.com:9443/myorg/myrepo.git:custom-name@feature-branch:abc123def"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$repo_ref" = "https://gitlab.example.com:9443/myorg/myrepo.git" ]
+ [ "$owner" = "myorg" ]
+ [ "$name" = "myrepo" ]
+ [ "$branch" = "feature-branch" ]
+ [ "$commit" = "abc123def" ]
+ [ "$url" = "https://gitlab.example.com:9443/myorg/myrepo.git" ]
+ [ "$dirname" = "custom-name" ]
+}
+
+@test "URL port parsing regression test - ensure ports are not confused with directory names" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # These are the problematic cases that the fix addresses
+ local test_cases=(
+ "https://example.com:8080/repo.git"
+ "https://gitlab.internal:9443/group/project.git"
+ "ssh://git@server.com:2222/owner/repo"
+ "https://git.company.com:8443/team/module.git"
+ )
+
+ for spec in "${test_cases[@]}"; do
+ run inst_parse_module_spec "$spec"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+
+ # Critical: dirname should NEVER be a port number
+ [[ ! "$dirname" =~ ^[0-9]+$ ]] || {
+ echo "FAIL: Port number '$dirname' incorrectly parsed as directory name for spec: $spec"
+ return 1
+ }
+
+ # dirname should be the repository name by default
+ local expected_name
+ if [[ "$spec" =~ /([^/]+)(\.git)?$ ]]; then
+ expected_name="${BASH_REMATCH[1]}"
+ expected_name="${expected_name%.git}"
+ fi
+ [ "$dirname" = "$expected_name" ] || {
+ echo "FAIL: Expected dirname '$expected_name' but got '$dirname' for spec: $spec"
+ return 1
+ }
+ done
+}
+
+@test "parsing should handle URL with custom directory but no branch" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ run inst_parse_module_spec "https://github.com/owner/repo.git:my-dir"
+ [ "$status" -eq 0 ]
+ IFS=' ' read -r repo_ref owner name branch commit url dirname <<< "$output"
+ [ "$repo_ref" = "https://github.com/owner/repo.git" ]
+ [ "$dirname" = "my-dir" ]
+ [ "$branch" = "-" ]
+}
+
+@test "modules list should maintain alphabetical order" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ # Add modules in random order
+ inst_mod_list_upsert "zeta/mod-z" "master" "abc"
+ inst_mod_list_upsert "alpha/mod-a" "master" "def"
+ inst_mod_list_upsert "beta/mod-b" "master" "ghi"
+
+ # Read the list and verify alphabetical order
+ local entries=()
+ while read -r repo_ref branch commit; do
+ [[ -z "$repo_ref" ]] && continue
+ entries+=("$repo_ref")
+ done < <(inst_mod_list_read)
+
+ # Should be in alphabetical order by owner/name
+ [ "${entries[0]}" = "alpha/mod-a" ]
+ [ "${entries[1]}" = "beta/mod-b" ]
+ [ "${entries[2]}" = "zeta/mod-z" ]
+}
+
+@test "module dispatcher should handle unknown commands gracefully" {
+ cd "$TEST_DIR"
+ source "$TEST_DIR/apps/installer/includes/includes.sh"
+
+ run inst_module "unknown-command"
+ [ "$status" -eq 1 ]
+ [[ "$output" =~ "Unknown module command" ]]
+} \ No newline at end of file