fix: Replace all echo with printf for reliable ANSI color output

- Changed color definitions from '\033[...]' to $'\033[...]'
  (actual escape bytes instead of literal strings)
- Replaced all display echo -e/echo -n/echo with printf
- Kept echo only for function return values and JSON output
- Fixes ANSI escape codes showing as raw text in list/inspect/monitor

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michel Abboud 2026-01-29 01:51:05 +00:00
parent 8864309c7d
commit d75f6b23b1

182
winctl.sh
View File

@ -26,16 +26,16 @@ readonly CACHE_MAX_AGE=$((7 * 24 * 60 * 60)) # 7 days in seconds
# ============================================================================== # ==============================================================================
if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then
readonly RED='\033[0;31m' readonly RED=$'\033[0;31m'
readonly GREEN='\033[0;32m' readonly GREEN=$'\033[0;32m'
readonly YELLOW='\033[0;33m' readonly YELLOW=$'\033[0;33m'
readonly BLUE='\033[0;34m' readonly BLUE=$'\033[0;34m'
readonly MAGENTA='\033[0;35m' readonly MAGENTA=$'\033[0;35m'
readonly CYAN='\033[0;36m' readonly CYAN=$'\033[0;36m'
readonly WHITE='\033[0;37m' readonly WHITE=$'\033[0;37m'
readonly BOLD='\033[1m' readonly BOLD=$'\033[1m'
readonly DIM='\033[2m' readonly DIM=$'\033[2m'
readonly RESET='\033[0m' readonly RESET=$'\033[0m'
else else
readonly RED='' readonly RED=''
readonly GREEN='' readonly GREEN=''
@ -147,19 +147,19 @@ readonly LEGACY_DISK_GB=32
# ============================================================================== # ==============================================================================
info() { info() {
echo -e "${BLUE}[INFO]${RESET} $*" printf '%s\n' "${BLUE}[INFO]${RESET} $*"
} }
success() { success() {
echo -e "${GREEN}[OK]${RESET} $*" printf '%s\n' "${GREEN}[OK]${RESET} $*"
} }
warn() { warn() {
echo -e "${YELLOW}[WARN]${RESET} $*" printf '%s\n' "${YELLOW}[WARN]${RESET} $*"
} }
error() { error() {
echo -e "${RED}[ERROR]${RESET} $*" >&2 printf '%s\n' "${RED}[ERROR]${RESET} $*" >&2
} }
die() { die() {
@ -168,9 +168,9 @@ die() {
} }
header() { header() {
echo "" printf '\n'
echo -e "${BOLD}${CYAN}$*${RESET}" printf '%s\n' "${BOLD}${CYAN}$*${RESET}"
echo -e "${DIM}$(printf '─%.0s' {1..60})${RESET}" printf '%s\n' "${DIM}$(printf '─%.0s' {1..60})${RESET}"
} }
# Print a formatted table row # Print a formatted table row
@ -188,15 +188,15 @@ table_row() {
*) status_color="${YELLOW}" ;; *) status_color="${YELLOW}" ;;
esac esac
printf " ${BOLD}%-12s${RESET} %-26s ${status_color}%-10s${RESET} %-8s %-8s\n" \ printf " %s%-12s%s %-26s %s%-10s%s %-8s %-8s\n" \
"$version" "$name" "$status" "$web" "$rdp" "${BOLD}" "$version" "${RESET}" "$name" "$status_color" "$status" "${RESET}" "$web" "$rdp"
} }
table_header() { table_header() {
echo "" printf '\n'
printf " ${BOLD}${DIM}%-12s %-26s %-10s %-8s %-8s${RESET}\n" \ printf " %s%-12s %-26s %-10s %-8s %-8s%s\n" \
"VERSION" "NAME" "STATUS" "WEB" "RDP" "${BOLD}${DIM}" "VERSION" "NAME" "STATUS" "WEB" "RDP" "${RESET}"
echo -e " ${DIM}$(printf '─%.0s' {1..66})${RESET}" printf '%s\n' " ${DIM}$(printf '─%.0s' {1..66})${RESET}"
} }
# ============================================================================== # ==============================================================================
@ -206,13 +206,13 @@ table_header() {
check_docker() { check_docker() {
if ! command -v docker &>/dev/null; then if ! command -v docker &>/dev/null; then
error "Docker is not installed" error "Docker is not installed"
echo " Install Docker: https://docs.docker.com/get-docker/" printf '%s\n' " Install Docker: https://docs.docker.com/get-docker/"
return 1 return 1
fi fi
if ! docker info &>/dev/null; then if ! docker info &>/dev/null; then
error "Docker daemon is not running" error "Docker daemon is not running"
echo " Start Docker: sudo systemctl start docker" printf '%s\n' " Start Docker: sudo systemctl start docker"
return 1 return 1
fi fi
@ -229,7 +229,7 @@ check_compose() {
return 0 return 0
else else
error "Docker Compose is not installed" error "Docker Compose is not installed"
echo " Install: https://docs.docker.com/compose/install/" printf '%s\n' " Install: https://docs.docker.com/compose/install/"
return 1 return 1
fi fi
} }
@ -237,13 +237,13 @@ check_compose() {
check_kvm() { check_kvm() {
if [[ ! -e /dev/kvm ]]; then if [[ ! -e /dev/kvm ]]; then
error "KVM device not found (/dev/kvm)" error "KVM device not found (/dev/kvm)"
echo " Enable virtualization in BIOS or check nested virtualization" printf '%s\n' " Enable virtualization in BIOS or check nested virtualization"
return 1 return 1
fi fi
if [[ ! -r /dev/kvm ]] || [[ ! -w /dev/kvm ]]; then if [[ ! -r /dev/kvm ]] || [[ ! -w /dev/kvm ]]; then
error "KVM device not accessible" error "KVM device not accessible"
echo " Fix: sudo usermod -aG kvm \$USER && newgrp kvm" printf '%s\n' " Fix: sudo usermod -aG kvm \$USER && newgrp kvm"
return 1 return 1
fi fi
@ -303,7 +303,7 @@ run_all_checks() {
check_memory || true # Warning only check_memory || true # Warning only
check_disk || true # Warning only check_disk || true # Warning only
echo "" printf '\n'
if ((failed > 0)); then if ((failed > 0)); then
error "Some critical checks failed. Please fix the issues above." error "Some critical checks failed. Please fix the issues above."
return 1 return 1
@ -729,11 +729,11 @@ cmd_start() {
fi fi
# Show connection info # Show connection info
echo "" printf '\n'
echo -e " ${BOLD}Connection Details:${RESET}" printf '%s\n' " ${BOLD}Connection Details:${RESET}"
echo -e " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}" printf '%s\n' " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}"
echo -e " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}" printf '%s\n' " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}"
echo "" printf '\n'
done done
# Refresh cache after state changes # Refresh cache after state changes
@ -776,8 +776,8 @@ cmd_stop() {
# Show confirmation # Show confirmation
header "Stopping Containers" header "Stopping Containers"
echo "" printf '\n'
echo " The following containers will be stopped:" printf '%s\n' " The following containers will be stopped:"
for v in "${versions[@]}"; do for v in "${versions[@]}"; do
local status local status
if is_running "$v"; then if is_running "$v"; then
@ -785,10 +785,10 @@ cmd_stop() {
else else
status="${YELLOW}not running${RESET}" status="${YELLOW}not running${RESET}"
fi fi
echo -e "$v (${VERSION_DISPLAY_NAMES[$v]}) - $status" printf '%s\n' "$v (${VERSION_DISPLAY_NAMES[$v]}) - $status"
done done
echo "" printf '\n'
echo -n " Continue? [y/N]: " printf '%s' " Continue? [y/N]: "
local confirm local confirm
read -r confirm read -r confirm
@ -838,11 +838,11 @@ cmd_restart() {
info "Restarting $v..." info "Restarting $v..."
if run_compose "$v" restart "$v"; then if run_compose "$v" restart "$v"; then
success "$v restarted" success "$v restarted"
echo "" printf '\n'
echo -e " ${BOLD}Connection Details:${RESET}" printf '%s\n' " ${BOLD}Connection Details:${RESET}"
echo -e " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}" printf '%s\n' " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}"
echo -e " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}" printf '%s\n' " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}"
echo "" printf '\n'
else else
error "Failed to restart $v" error "Failed to restart $v"
fi fi
@ -871,7 +871,7 @@ cmd_status() {
status=$(get_status "$v") status=$(get_status "$v")
table_row "$v" "${VERSION_DISPLAY_NAMES[$v]}" "$status" "${VERSION_PORTS_WEB[$v]}" "${VERSION_PORTS_RDP[$v]}" table_row "$v" "${VERSION_DISPLAY_NAMES[$v]}" "$status" "${VERSION_PORTS_WEB[$v]}" "${VERSION_PORTS_RDP[$v]}"
done done
echo "" printf '\n'
} }
cmd_logs() { cmd_logs() {
@ -977,15 +977,15 @@ cmd_rebuild() {
# Show warning # Show warning
header "⚠️ Rebuild Containers" header "⚠️ Rebuild Containers"
echo "" printf '\n'
echo -e " ${RED}${BOLD}WARNING: This will destroy and recreate the following containers.${RESET}" printf '%s\n' " ${RED}${BOLD}WARNING: This will destroy and recreate the following containers.${RESET}"
echo -e " ${RED}Data in /storage volumes will be preserved.${RESET}" printf '%s\n' " ${RED}Data in /storage volumes will be preserved.${RESET}"
echo "" printf '\n'
for v in "${versions[@]}"; do for v in "${versions[@]}"; do
echo "$v (${VERSION_DISPLAY_NAMES[$v]})" printf '%s\n' "$v (${VERSION_DISPLAY_NAMES[$v]})"
done done
echo "" printf '\n'
echo -n " Type 'yes' to confirm: " printf '%s' " Type 'yes' to confirm: "
local confirm local confirm
read -r confirm read -r confirm
@ -1010,11 +1010,11 @@ cmd_rebuild() {
info "Recreating $v..." info "Recreating $v..."
if run_compose "$v" up -d "$v"; then if run_compose "$v" up -d "$v"; then
success "$v rebuilt successfully" success "$v rebuilt successfully"
echo "" printf '\n'
echo -e " ${BOLD}Connection Details:${RESET}" printf '%s\n' " ${BOLD}Connection Details:${RESET}"
echo -e " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}" printf '%s\n' " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}"
echo -e " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}" printf '%s\n' " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}"
echo "" printf '\n'
else else
error "Failed to rebuild $v" error "Failed to rebuild $v"
fi fi
@ -1042,11 +1042,11 @@ cmd_list() {
esac esac
for cat in "${categories[@]}"; do for cat in "${categories[@]}"; do
echo "" printf '\n'
local cat_upper local cat_upper
cat_upper=$(echo "$cat" | tr '[:lower:]' '[:upper:]') cat_upper=$(echo "$cat" | tr '[:lower:]' '[:upper:]')
echo -e " ${BOLD}${cat_upper}${RESET}" printf '%s\n' " ${BOLD}${cat_upper}${RESET}"
echo -e " ${DIM}$(printf '─%.0s' {1..50})${RESET}" printf '%s\n' " ${DIM}$(printf '─%.0s' {1..50})${RESET}"
for v in "${ALL_VERSIONS[@]}"; do for v in "${ALL_VERSIONS[@]}"; do
if [[ "${VERSION_CATEGORIES[$v]}" == "$cat" ]]; then if [[ "${VERSION_CATEGORIES[$v]}" == "$cat" ]]; then
@ -1066,7 +1066,7 @@ cmd_list() {
fi fi
done done
done done
echo "" printf '\n'
} }
cmd_inspect() { cmd_inspect() {
@ -1079,19 +1079,19 @@ cmd_inspect() {
validate_version "$version" || exit 1 validate_version "$version" || exit 1
header "Container Details: $version" header "Container Details: $version"
echo "" printf '\n'
echo -e " ${BOLD}Version:${RESET} $version" printf '%s\n' " ${BOLD}Version:${RESET} $version"
echo -e " ${BOLD}Name:${RESET} ${VERSION_DISPLAY_NAMES[$version]}" printf '%s\n' " ${BOLD}Name:${RESET} ${VERSION_DISPLAY_NAMES[$version]}"
echo -e " ${BOLD}Category:${RESET} ${VERSION_CATEGORIES[$version]}" printf '%s\n' " ${BOLD}Category:${RESET} ${VERSION_CATEGORIES[$version]}"
echo -e " ${BOLD}Status:${RESET} $(get_status "$version")" printf '%s\n' " ${BOLD}Status:${RESET} $(get_status "$version")"
echo -e " ${BOLD}Web Port:${RESET} ${VERSION_PORTS_WEB[$version]}" printf '%s\n' " ${BOLD}Web Port:${RESET} ${VERSION_PORTS_WEB[$version]}"
echo -e " ${BOLD}RDP Port:${RESET} ${VERSION_PORTS_RDP[$version]}" printf '%s\n' " ${BOLD}RDP Port:${RESET} ${VERSION_PORTS_RDP[$version]}"
echo -e " ${BOLD}Resources:${RESET} ${VERSION_RESOURCE_TYPE[$version]}" printf '%s\n' " ${BOLD}Resources:${RESET} ${VERSION_RESOURCE_TYPE[$version]}"
echo -e " ${BOLD}Compose:${RESET} ${VERSION_COMPOSE_FILES[$version]}" printf '%s\n' " ${BOLD}Compose:${RESET} ${VERSION_COMPOSE_FILES[$version]}"
echo "" printf '\n'
if container_exists "$version"; then if container_exists "$version"; then
echo -e " ${BOLD}Docker Info:${RESET}" printf '%s\n' " ${BOLD}Docker Info:${RESET}"
docker inspect "$version" --format ' docker inspect "$version" --format '
Image: {{.Config.Image}} Image: {{.Config.Image}}
Created: {{.Created}} Created: {{.Created}}
@ -1099,7 +1099,7 @@ cmd_inspect() {
Mounts: {{range .Mounts}}{{.Source}} -> {{.Destination}} Mounts: {{range .Mounts}}{{.Source}} -> {{.Destination}}
{{end}}' 2>/dev/null || true {{end}}' 2>/dev/null || true
fi fi
echo "" printf '\n'
} }
cmd_monitor() { cmd_monitor() {
@ -1110,16 +1110,16 @@ cmd_monitor() {
fi fi
header "Real-time Monitor (refresh: ${interval}s)" header "Real-time Monitor (refresh: ${interval}s)"
echo " Press Ctrl+C to exit" printf '%s\n' " Press Ctrl+C to exit"
echo "" printf '\n'
while true; do while true; do
# Refresh cache for accurate status # Refresh cache for accurate status
invalidate_cache invalidate_cache
clear clear
echo -e "${BOLD}${CYAN}Windows Container Monitor${RESET} - $(date '+%Y-%m-%d %H:%M:%S')" printf '%s\n' "${BOLD}${CYAN}Windows Container Monitor${RESET} - $(date '+%Y-%m-%d %H:%M:%S')"
echo -e "${DIM}$(printf '─%.0s' {1..70})${RESET}" printf '%s\n' "${DIM}$(printf '─%.0s' {1..70})${RESET}"
local running_count=0 local running_count=0
local stopped_count=0 local stopped_count=0
@ -1142,13 +1142,13 @@ cmd_monitor() {
done done
if [[ $total_count -eq 0 ]]; then if [[ $total_count -eq 0 ]]; then
echo -e " ${DIM}No containers found${RESET}" printf '%s\n' " ${DIM}No containers found${RESET}"
fi fi
echo "" printf '\n'
echo -e " ${BOLD}Summary:${RESET} ${GREEN}$running_count running${RESET}, ${RED}$stopped_count stopped${RESET}, $total_count total" printf '%s\n' " ${BOLD}Summary:${RESET} ${GREEN}$running_count running${RESET}, ${RED}$stopped_count stopped${RESET}, $total_count total"
echo "" printf '\n'
echo -e " ${DIM}Refreshing in ${interval}s... (Ctrl+C to exit)${RESET}" printf '%s\n' " ${DIM}Refreshing in ${interval}s... (Ctrl+C to exit)${RESET}"
sleep "$interval" sleep "$interval"
done done
@ -1170,12 +1170,12 @@ cmd_refresh() {
# Show cache info # Show cache info
local age local age
age=$(get_cache_age) age=$(get_cache_age)
echo "" printf '\n'
echo -e " ${BOLD}Cache Info:${RESET}" printf '%s\n' " ${BOLD}Cache Info:${RESET}"
echo -e " → File: ${CYAN}${CACHE_FILE}${RESET}" printf '%s\n' " → File: ${CYAN}${CACHE_FILE}${RESET}"
echo -e " → Age: ${age} seconds" printf '%s\n' " → Age: ${age} seconds"
echo -e " → Max Age: ${CACHE_MAX_AGE} seconds (7 days)" printf '%s\n' " → Max Age: ${CACHE_MAX_AGE} seconds (7 days)"
echo "" printf '\n'
# Show summary # Show summary
local cnt_running=0 cnt_stopped=0 cnt_other=0 local cnt_running=0 cnt_stopped=0 cnt_other=0
@ -1186,8 +1186,8 @@ cmd_refresh() {
*) ((cnt_other++)) || true ;; *) ((cnt_other++)) || true ;;
esac esac
done done
echo -e " ${BOLD}Containers:${RESET} ${GREEN}${cnt_running} running${RESET}, ${RED}${cnt_stopped} stopped${RESET}, ${DIM}${cnt_other} other${RESET}" printf '%s\n' " ${BOLD}Containers:${RESET} ${GREEN}${cnt_running} running${RESET}, ${RED}${cnt_stopped} stopped${RESET}, ${DIM}${cnt_other} other${RESET}"
echo "" printf '\n'
} }
# ============================================================================== # ==============================================================================
@ -1276,7 +1276,7 @@ main() {
;; ;;
*) *)
error "Unknown command: $command" error "Unknown command: $command"
echo "Run '${SCRIPT_NAME} help' for usage information" printf '%s\n' "Run '${SCRIPT_NAME} help' for usage information"
exit 1 exit 1
;; ;;
esac esac