501 lines
17 KiB
Bash
Executable File
501 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
repo_root="$(cd "${script_dir}/../.." && pwd)"
|
|
source "${script_dir}/../lib-nextcloud-apps-path.sh"
|
|
|
|
occ_path=""
|
|
nextcloud_path=""
|
|
output_mode="text"
|
|
output_file=""
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: ./scripts/topology/analyze-existing-nextcloud-instance.sh [options]
|
|
|
|
Read-only host analysis for adopting an existing Nextcloud instance into the
|
|
NuQloud/Qortal integration stack.
|
|
|
|
Options:
|
|
--occ-path <path> Explicit path to Nextcloud occ
|
|
--nextcloud-path <path> Explicit Nextcloud root path (used to derive occ)
|
|
--json JSON output
|
|
--text Human-readable output (default)
|
|
--output-file <path> Write output to a file instead of stdout
|
|
-h, --help Show this help
|
|
USAGE
|
|
}
|
|
|
|
json_escape() {
|
|
local input="${1:-}"
|
|
input="${input//\\/\\\\}"
|
|
input="${input//\"/\\\"}"
|
|
input="${input//$'\n'/\\n}"
|
|
input="${input//$'\r'/}"
|
|
input="${input//$'\t'/\\t}"
|
|
printf '%s' "${input}"
|
|
}
|
|
|
|
bool_json() {
|
|
if [[ "${1:-0}" == "1" ]]; then
|
|
printf 'true'
|
|
else
|
|
printf 'false'
|
|
fi
|
|
}
|
|
|
|
join_lines_json() {
|
|
local input="${1:-}"
|
|
local first="1"
|
|
local line result=""
|
|
while IFS= read -r line; do
|
|
[[ -z "${line}" ]] && continue
|
|
if [[ "${first}" == "1" ]]; then
|
|
result="\"$(json_escape "${line}")\""
|
|
first="0"
|
|
else
|
|
result="${result},\"$(json_escape "${line}")\""
|
|
fi
|
|
done <<< "${input}"
|
|
printf '%s' "${result}"
|
|
}
|
|
|
|
apps_paths_json() {
|
|
local input="${1:-}"
|
|
local first="1"
|
|
local path=""
|
|
local url=""
|
|
local writable=""
|
|
while IFS=$'\t' read -r path url writable; do
|
|
[[ -n "${path}" ]] || continue
|
|
if [[ "${first}" == "1" ]]; then
|
|
first="0"
|
|
else
|
|
printf ','
|
|
fi
|
|
printf '{"path":"%s","url":"%s","writable":%s}' \
|
|
"$(json_escape "${path}")" \
|
|
"$(json_escape "${url}")" \
|
|
"$(bool_json "${writable}")"
|
|
done <<< "${input}"
|
|
}
|
|
|
|
append_line() {
|
|
local var_name="$1"
|
|
local line="${2:-}"
|
|
local current="${!var_name:-}"
|
|
if [[ -z "${line}" ]]; then
|
|
return 0
|
|
fi
|
|
if [[ -z "${current}" ]]; then
|
|
printf -v "${var_name}" '%s' "${line}"
|
|
else
|
|
printf -v "${var_name}" '%s\n%s' "${current}" "${line}"
|
|
fi
|
|
}
|
|
|
|
run_timeout_cmd() {
|
|
if command -v timeout >/dev/null 2>&1; then
|
|
timeout 20 "$@"
|
|
else
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
detect_occ_path() {
|
|
local candidates=(
|
|
"/var/www/nextcloud/occ"
|
|
"/var/www/html/nextcloud/occ"
|
|
"/var/www/html/occ"
|
|
"/srv/www/nextcloud/occ"
|
|
"/var/lib/nextcloud/occ"
|
|
)
|
|
local candidate
|
|
for candidate in "${candidates[@]}"; do
|
|
if [[ -f "${candidate}" ]]; then
|
|
printf '%s\n' "${candidate}"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
occ_runner() {
|
|
if [[ -z "${occ_path}" || ! -f "${occ_path}" ]]; then
|
|
return 1
|
|
fi
|
|
if [[ -z "$(command -v php || true)" ]]; then
|
|
return 1
|
|
fi
|
|
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
|
|
if command -v sudo >/dev/null 2>&1; then
|
|
run_timeout_cmd sudo -u www-data php "${occ_path}" "$@"
|
|
return $?
|
|
fi
|
|
if command -v runuser >/dev/null 2>&1; then
|
|
run_timeout_cmd runuser -u www-data -- php "${occ_path}" "$@"
|
|
return $?
|
|
fi
|
|
fi
|
|
run_timeout_cmd php "${occ_path}" "$@"
|
|
}
|
|
|
|
detect_service_state() {
|
|
local service_name="${1:-}"
|
|
if [[ -z "${service_name}" || -z "$(command -v systemctl || true)" ]]; then
|
|
return 1
|
|
fi
|
|
systemctl is-active "${service_name}" 2>/dev/null || true
|
|
}
|
|
|
|
docker_access_cmd=""
|
|
docker_daemon_reachable="0"
|
|
docker_access_mode="unavailable"
|
|
|
|
ensure_docker_best_effort() {
|
|
local docker_path=""
|
|
docker_path="$(type -P docker || true)"
|
|
if [[ -z "${docker_path}" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if "${docker_path}" info >/dev/null 2>&1; then
|
|
docker_access_cmd="${docker_path}"
|
|
docker_daemon_reachable="1"
|
|
docker_access_mode="direct"
|
|
return 0
|
|
fi
|
|
|
|
if command -v sudo >/dev/null 2>&1 && sudo -n "${docker_path}" info >/dev/null 2>&1; then
|
|
docker_access_cmd="sudo ${docker_path}"
|
|
docker_daemon_reachable="1"
|
|
docker_access_mode="sudo"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
run_docker_cmd() {
|
|
if [[ -z "${docker_access_cmd}" ]]; then
|
|
return 1
|
|
fi
|
|
bash -lc "${docker_access_cmd} $*"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--occ-path)
|
|
occ_path="${2:-}"
|
|
shift 2
|
|
;;
|
|
--occ-path=*)
|
|
occ_path="${1#*=}"
|
|
shift
|
|
;;
|
|
--nextcloud-path)
|
|
nextcloud_path="${2:-}"
|
|
shift 2
|
|
;;
|
|
--nextcloud-path=*)
|
|
nextcloud_path="${1#*=}"
|
|
shift
|
|
;;
|
|
--json)
|
|
output_mode="json"
|
|
shift
|
|
;;
|
|
--text)
|
|
output_mode="text"
|
|
shift
|
|
;;
|
|
--output-file)
|
|
output_file="${2:-}"
|
|
shift 2
|
|
;;
|
|
--output-file=*)
|
|
output_file="${1#*=}"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -n "${nextcloud_path}" && "${nextcloud_path}" != /* ]]; then
|
|
nextcloud_path="$(cd "${nextcloud_path}" 2>/dev/null && pwd || true)"
|
|
fi
|
|
if [[ -n "${occ_path}" && "${occ_path}" != /* ]]; then
|
|
occ_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd)/$(basename "${occ_path}")"
|
|
fi
|
|
|
|
if [[ -z "${occ_path}" && -n "${nextcloud_path}" && -f "${nextcloud_path}/occ" ]]; then
|
|
occ_path="${nextcloud_path}/occ"
|
|
fi
|
|
if [[ -z "${occ_path}" ]]; then
|
|
occ_path="$(detect_occ_path || true)"
|
|
fi
|
|
if [[ -n "${occ_path}" && -z "${nextcloud_path}" ]]; then
|
|
nextcloud_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd || true)"
|
|
fi
|
|
|
|
host_name="$(hostname 2>/dev/null || true)"
|
|
kernel_name="$(uname -sr 2>/dev/null || true)"
|
|
current_user="$(id -un 2>/dev/null || true)"
|
|
php_path="$(command -v php || true)"
|
|
php_version="$(php -v 2>/dev/null | head -n1 || true)"
|
|
|
|
apache_state="$(detect_service_state apache2)"
|
|
httpd_state="$(detect_service_state httpd)"
|
|
nginx_state="$(detect_service_state nginx)"
|
|
caddy_state="$(detect_service_state caddy)"
|
|
docker_state="$(detect_service_state docker)"
|
|
mariadb_state="$(detect_service_state mariadb)"
|
|
mysql_state="$(detect_service_state mysql)"
|
|
postgres_state="$(detect_service_state postgresql)"
|
|
redis_state="$(detect_service_state redis-server)"
|
|
|
|
listening_ports=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
listening_ports="$(ss -ltn 2>/dev/null | awk 'NR==1 || /:80 |:80$|:443 |:443$|:8080 |:8080$|:8081 |:8081$|:9980 |:9980$|:3000 |:3000$|:3191 |:3191$|:1234 |:1234$|:12391 |:12391$|:3478 |:3478$|:3479 |:3479$/' || true)"
|
|
fi
|
|
|
|
occ_found="0"
|
|
occ_status_ok="0"
|
|
occ_status_json=""
|
|
occ_status_text=""
|
|
nc_version=""
|
|
nc_installed=""
|
|
nc_maintenance=""
|
|
nc_needs_db_upgrade=""
|
|
nc_overwrite_url=""
|
|
nc_dbtype=""
|
|
nc_dbhost=""
|
|
nc_datadirectory=""
|
|
nc_trusted_domain_0=""
|
|
nc_enabled_apps=""
|
|
nc_apps_of_interest=""
|
|
nc_apps_paths=""
|
|
nc_selected_apps_path=""
|
|
|
|
if [[ -n "${occ_path}" && -f "${occ_path}" ]]; then
|
|
occ_found="1"
|
|
occ_status_json="$(occ_runner status --output=json 2>/dev/null || true)"
|
|
if [[ -n "${occ_status_json}" ]]; then
|
|
occ_status_ok="1"
|
|
fi
|
|
occ_status_text="$(occ_runner status 2>/dev/null || true)"
|
|
nc_version="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- versionstring:[[:space:]]*//p' | head -n1)"
|
|
nc_installed="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- installed:[[:space:]]*//p' | head -n1)"
|
|
nc_maintenance="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- maintenance:[[:space:]]*//p' | head -n1)"
|
|
nc_needs_db_upgrade="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- needsDbUpgrade:[[:space:]]*//p' | head -n1)"
|
|
nc_overwrite_url="$(occ_runner config:system:get overwrite.cli.url 2>/dev/null | tr -d '\r' | head -n1 || true)"
|
|
nc_dbtype="$(occ_runner config:system:get dbtype 2>/dev/null | tr -d '\r' | head -n1 || true)"
|
|
nc_dbhost="$(occ_runner config:system:get dbhost 2>/dev/null | tr -d '\r' | head -n1 || true)"
|
|
nc_datadirectory="$(occ_runner config:system:get datadirectory 2>/dev/null | tr -d '\r' | head -n1 || true)"
|
|
nc_trusted_domain_0="$(occ_runner config:system:get trusted_domains 0 2>/dev/null | tr -d '\r' | head -n1 || true)"
|
|
nc_enabled_apps="$(occ_runner app:list 2>/dev/null || true)"
|
|
nc_apps_of_interest="$(printf '%s\n' "${nc_enabled_apps}" | grep -E 'qortal_integration|user_oidc|notify_push|spreed|richdocuments|custom_pwa|chd_admin|qortal_files_bridge|qortal_talk_bridge|nuqloud_scrum' || true)"
|
|
nc_apps_paths="$(nextcloud_apps_paths_from_config "${nextcloud_path}" 2>/dev/null || true)"
|
|
nc_selected_apps_path="$(nextcloud_select_apps_path "${nextcloud_path}" 2>/dev/null || true)"
|
|
fi
|
|
|
|
docker_cli_present="0"
|
|
if command -v docker >/dev/null 2>&1; then
|
|
docker_cli_present="1"
|
|
fi
|
|
ensure_docker_best_effort || true
|
|
|
|
docker_ps_summary=""
|
|
docker_qortal_related=""
|
|
docker_networks=""
|
|
if [[ "${docker_daemon_reachable}" == "1" ]]; then
|
|
docker_ps_summary="$(run_docker_cmd "ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}'" 2>/dev/null || true)"
|
|
docker_qortal_related="$(printf '%s\n' "${docker_ps_summary}" | grep -Ei 'qortal|broker|external_auth|nextcloud|collabora|talk|caddy' || true)"
|
|
docker_networks="$(run_docker_cmd "network ls --format '{{.Name}}'" 2>/dev/null || true)"
|
|
fi
|
|
|
|
topology_id="unknown"
|
|
topology_reason="Unable to classify runtime yet."
|
|
recommended_target="manual-analysis"
|
|
recommended_next_step="./scripts/analyze-existing-nextcloud-instance.sh --text"
|
|
|
|
if [[ "${occ_found}" == "1" && "${docker_daemon_reachable}" == "1" ]]; then
|
|
topology_id="host-nextcloud-plus-docker"
|
|
topology_reason="Detected host-level Nextcloud OCC and a reachable Docker daemon."
|
|
recommended_target="host-nextcloud-hybrid"
|
|
recommended_next_step="./scripts/install-host-nextcloud-hybrid.sh --occ-path ${occ_path}"
|
|
elif [[ "${occ_found}" == "1" ]]; then
|
|
topology_id="host-nextcloud-no-docker"
|
|
topology_reason="Detected host-level Nextcloud OCC without usable Docker access."
|
|
recommended_target="app-only-or-external-services"
|
|
recommended_next_step="Provision broker/external-auth/Qortal services separately, then wire Nextcloud with OCC."
|
|
elif [[ "${docker_daemon_reachable}" == "1" && -n "${docker_qortal_related}" ]]; then
|
|
topology_id="docker-runtime-no-host-occ"
|
|
topology_reason="Detected Docker daemon and Nextcloud/Qortal sidecar containers, but no host-level OCC path."
|
|
recommended_target="repo-devprod-or-custom-compose"
|
|
recommended_next_step="./scripts/topology/discover-cloud-topology.sh --text"
|
|
fi
|
|
|
|
report_text() {
|
|
cat <<EOF
|
|
Existing Nextcloud Instance Analysis
|
|
|
|
Host:
|
|
hostname: ${host_name:-<unknown>}
|
|
kernel: ${kernel_name:-<unknown>}
|
|
user: ${current_user:-<unknown>}
|
|
php: ${php_path:-<missing>}
|
|
php_version: ${php_version:-<unknown>}
|
|
|
|
Host services:
|
|
apache2: ${apache_state:-<unknown>}
|
|
httpd: ${httpd_state:-<unknown>}
|
|
nginx: ${nginx_state:-<unknown>}
|
|
caddy: ${caddy_state:-<unknown>}
|
|
docker: ${docker_state:-<unknown>}
|
|
mariadb: ${mariadb_state:-<unknown>}
|
|
mysql: ${mysql_state:-<unknown>}
|
|
postgresql: ${postgres_state:-<unknown>}
|
|
redis-server: ${redis_state:-<unknown>}
|
|
|
|
Nextcloud host detection:
|
|
occ_found: ${occ_found}
|
|
occ_path: ${occ_path:-<not found>}
|
|
nextcloud_path: ${nextcloud_path:-<unknown>}
|
|
occ_status_ok: ${occ_status_ok}
|
|
installed: ${nc_installed:-<unknown>}
|
|
version: ${nc_version:-<unknown>}
|
|
maintenance: ${nc_maintenance:-<unknown>}
|
|
needsDbUpgrade: ${nc_needs_db_upgrade:-<unknown>}
|
|
overwrite.cli.url: ${nc_overwrite_url:-<empty>}
|
|
trusted_domains[0]: ${nc_trusted_domain_0:-<empty>}
|
|
dbtype: ${nc_dbtype:-<empty>}
|
|
dbhost: ${nc_dbhost:-<empty>}
|
|
datadirectory: ${nc_datadirectory:-<empty>}
|
|
|
|
Apps paths:
|
|
$(if [[ -n "${nc_apps_paths}" ]]; then printf '%s\n' "${nc_apps_paths}" | while IFS=$'\t' read -r path url writable; do
|
|
[[ -n "${path}" ]] || continue
|
|
printf ' - path=%s url=%s writable=%s\n' "${path}" "${url:-<empty>}" "$( [[ "${writable}" == "1" ]] && echo true || echo false )"
|
|
done; else echo " <none detected>"; fi)
|
|
selected_apps_path: ${nc_selected_apps_path:-<default>}
|
|
|
|
Apps of interest:
|
|
$(if [[ -n "${nc_apps_of_interest}" ]]; then printf '%s\n' "${nc_apps_of_interest}" | sed 's/^/ /'; else echo " <none detected>"; fi)
|
|
|
|
Docker:
|
|
cli_present: ${docker_cli_present}
|
|
daemon_reachable: ${docker_daemon_reachable}
|
|
access_mode: ${docker_access_mode}
|
|
|
|
Qortal/Nextcloud-related containers:
|
|
$(if [[ -n "${docker_qortal_related}" ]]; then printf '%s\n' "${docker_qortal_related}" | sed 's/^/ /'; else echo " <none detected>"; fi)
|
|
|
|
Docker networks:
|
|
$(if [[ -n "${docker_networks}" ]]; then printf '%s\n' "${docker_networks}" | sed 's/^/ /'; else echo " <none detected>"; fi)
|
|
|
|
Listening ports snapshot:
|
|
$(if [[ -n "${listening_ports}" ]]; then printf '%s\n' "${listening_ports}" | sed 's/^/ /'; else echo " <not available>"; fi)
|
|
|
|
Recommendation:
|
|
topology_id: ${topology_id}
|
|
reason: ${topology_reason}
|
|
target_profile: ${recommended_target}
|
|
next_step: ${recommended_next_step}
|
|
|
|
Suggested rollout stages:
|
|
1. Run this analyzer on the target host and save the output.
|
|
2. Decide whether the target is:
|
|
- full NuQloud sidecar stack on an existing Nextcloud host
|
|
- hybrid host-Nextcloud plus Docker sidecars
|
|
- app-only/manual external services
|
|
3. Reuse existing scripts where possible:
|
|
- scripts/topology/discover-cloud-topology.sh
|
|
- scripts/topology/apply-topology-profile.sh
|
|
- scripts/nextcloud-vm-install.sh
|
|
- scripts/finish-initial-setup.sh
|
|
- scripts/ensure-*.sh helpers for URL, Talk, service auth, CustomPWA, qortal runtime
|
|
4. Use the host-hybrid installer and lifecycle wrappers after confirming the target topology.
|
|
EOF
|
|
}
|
|
|
|
report_json() {
|
|
printf '{\n'
|
|
printf ' "ok": true,\n'
|
|
printf ' "generatedAt": "%s",\n' "$(json_escape "$(date -u +%Y-%m-%dT%H:%M:%SZ)")"
|
|
printf ' "host": {\n'
|
|
printf ' "hostname": "%s",\n' "$(json_escape "${host_name}")"
|
|
printf ' "kernel": "%s",\n' "$(json_escape "${kernel_name}")"
|
|
printf ' "user": "%s",\n' "$(json_escape "${current_user}")"
|
|
printf ' "phpPath": "%s",\n' "$(json_escape "${php_path}")"
|
|
printf ' "phpVersion": "%s"\n' "$(json_escape "${php_version}")"
|
|
printf ' },\n'
|
|
printf ' "services": {\n'
|
|
printf ' "apache2": "%s",\n' "$(json_escape "${apache_state}")"
|
|
printf ' "httpd": "%s",\n' "$(json_escape "${httpd_state}")"
|
|
printf ' "nginx": "%s",\n' "$(json_escape "${nginx_state}")"
|
|
printf ' "caddy": "%s",\n' "$(json_escape "${caddy_state}")"
|
|
printf ' "docker": "%s",\n' "$(json_escape "${docker_state}")"
|
|
printf ' "mariadb": "%s",\n' "$(json_escape "${mariadb_state}")"
|
|
printf ' "mysql": "%s",\n' "$(json_escape "${mysql_state}")"
|
|
printf ' "postgresql": "%s",\n' "$(json_escape "${postgres_state}")"
|
|
printf ' "redisServer": "%s"\n' "$(json_escape "${redis_state}")"
|
|
printf ' },\n'
|
|
printf ' "nextcloud": {\n'
|
|
printf ' "occFound": %s,\n' "$(bool_json "${occ_found}")"
|
|
printf ' "occPath": "%s",\n' "$(json_escape "${occ_path}")"
|
|
printf ' "nextcloudPath": "%s",\n' "$(json_escape "${nextcloud_path}")"
|
|
printf ' "occStatusOk": %s,\n' "$(bool_json "${occ_status_ok}")"
|
|
printf ' "installed": "%s",\n' "$(json_escape "${nc_installed}")"
|
|
printf ' "version": "%s",\n' "$(json_escape "${nc_version}")"
|
|
printf ' "maintenance": "%s",\n' "$(json_escape "${nc_maintenance}")"
|
|
printf ' "needsDbUpgrade": "%s",\n' "$(json_escape "${nc_needs_db_upgrade}")"
|
|
printf ' "overwriteCliUrl": "%s",\n' "$(json_escape "${nc_overwrite_url}")"
|
|
printf ' "trustedDomain0": "%s",\n' "$(json_escape "${nc_trusted_domain_0}")"
|
|
printf ' "dbtype": "%s",\n' "$(json_escape "${nc_dbtype}")"
|
|
printf ' "dbhost": "%s",\n' "$(json_escape "${nc_dbhost}")"
|
|
printf ' "datadirectory": "%s",\n' "$(json_escape "${nc_datadirectory}")"
|
|
printf ' "appsPaths": [%s],\n' "$(apps_paths_json "${nc_apps_paths}")"
|
|
printf ' "selectedAppsPath": "%s",\n' "$(json_escape "${nc_selected_apps_path}")"
|
|
printf ' "appsOfInterest": [%s]\n' "$(join_lines_json "${nc_apps_of_interest}")"
|
|
printf ' },\n'
|
|
printf ' "docker": {\n'
|
|
printf ' "cliPresent": %s,\n' "$(bool_json "${docker_cli_present}")"
|
|
printf ' "daemonReachable": %s,\n' "$(bool_json "${docker_daemon_reachable}")"
|
|
printf ' "accessMode": "%s",\n' "$(json_escape "${docker_access_mode}")"
|
|
printf ' "qortalRelatedContainers": [%s],\n' "$(join_lines_json "${docker_qortal_related}")"
|
|
printf ' "networks": [%s]\n' "$(join_lines_json "${docker_networks}")"
|
|
printf ' },\n'
|
|
printf ' "listeningPorts": [%s],\n' "$(join_lines_json "${listening_ports}")"
|
|
printf ' "recommendation": {\n'
|
|
printf ' "topologyId": "%s",\n' "$(json_escape "${topology_id}")"
|
|
printf ' "reason": "%s",\n' "$(json_escape "${topology_reason}")"
|
|
printf ' "targetProfile": "%s",\n' "$(json_escape "${recommended_target}")"
|
|
printf ' "nextStep": "%s"\n' "$(json_escape "${recommended_next_step}")"
|
|
printf ' }\n'
|
|
printf '}\n'
|
|
}
|
|
|
|
rendered_output=""
|
|
if [[ "${output_mode}" == "json" ]]; then
|
|
rendered_output="$(report_json)"
|
|
else
|
|
rendered_output="$(report_text)"
|
|
fi
|
|
|
|
if [[ -n "${output_file}" ]]; then
|
|
mkdir -p "$(dirname "${output_file}")"
|
|
printf '%s\n' "${rendered_output}" > "${output_file}"
|
|
else
|
|
printf '%s\n' "${rendered_output}"
|
|
fi
|