Browse Source

feat(core): use plain line-based output when stdout is not a terminal (#5117)

Output captured by systemd/journald, cron, pipes or redirected files was
full of colour codes, carriage returns and erase-line sequences, and
in-place progress messages overwrote each other instead of producing
complete lines.

When stdout is not a terminal, LinuxGSM now omits all terminal control
sequences. In-place status messages are held until they are replaced,
completed by an end-of-line status or flushed, so each step is written
as one complete line showing its final result. Held messages are
flushed before headers, prompts and on exit.

ansi="off" now consistently disables ANSI formatting: the erase-line
sequence is no longer re-added unconditionally and text styles are only
set when ANSI is enabled.

Direct terminal control outside the shared helpers is also skipped in
plain mode: the download progress bar, the dependency install spinner
and tput cursor handling in fastdl and mods-remove.
Daniel Gibbs 5 ngày trước cách đây
mục cha
commit
68d26976d8

+ 18 - 14
lgsm/modules/check_deps.sh

@@ -39,13 +39,15 @@ fn_install_mono_repo() {
 	if [ "${autodepinstall}" == "0" ]; then
 		fn_print_information_nl "Automatically adding Mono repository."
 		fn_script_log_info "Automatically adding Mono repository."
-		echo -en ".\r"
-		fn_sleep_time_1
-		echo -en "..\r"
-		fn_sleep_time_1
-		echo -en "...\r"
-		fn_sleep_time_1
-		echo -en "   \r"
+		if [ -z "${plainoutput}" ]; then
+			echo -en ".\r"
+			fn_sleep_time_1
+			echo -en "..\r"
+			fn_sleep_time_1
+			echo -en "...\r"
+			fn_sleep_time_1
+			echo -en "   \r"
+		fi
 		if [ "${distroid}" == "ubuntu" ]; then
 			if [ "${distroversion}" == "22.04" ]; then
 				cmd="sudo apt-get install gnupg ca-certificates;sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF;echo 'deb https://download.mono-project.com/repo/ubuntu stable-jammy main' | sudo tee /etc/apt/sources.list.d/mono-official-stable.list;sudo apt update"
@@ -201,13 +203,15 @@ fn_install_missing_deps() {
 		if [ "${autodepinstall}" == "0" ]; then
 			fn_print_information_nl "Automatically installing missing dependencies."
 			fn_script_log_info "Automatically installing missing dependencies."
-			echo -en ".\r"
-			fn_sleep_time_1
-			echo -en "..\r"
-			fn_sleep_time_1
-			echo -en "...\r"
-			fn_sleep_time_1
-			echo -en "   \r"
+			if [ -z "${plainoutput}" ]; then
+				echo -en ".\r"
+				fn_sleep_time_1
+				echo -en "..\r"
+				fn_sleep_time_1
+				echo -en "...\r"
+				fn_sleep_time_1
+				echo -en "   \r"
+			fi
 			if [ "$(command -v apt 2> /dev/null)" ]; then
 				cmd="echo steamcmd steam/question select \"I AGREE\" | sudo debconf-set-selections; echo steamcmd steam/license note '' | sudo debconf-set-selections; ${i386installcommand}sudo apt-get update; sudo apt-get -y install ${array_deps_missing[*]}"
 				eval "${cmd}"

+ 36 - 14
lgsm/modules/command_fastdl.sh

@@ -194,17 +194,23 @@ fn_fastdl_gather() {
 	fi
 	for allowed_extention in "${allowed_extentions_array[@]}"; do
 		fileswc=0
-		tput sc
+		if [ -z "${plainoutput}" ]; then
+			tput sc
+		fi
 		while IFS=$'\t' read -r filesize filemtime fastdlfile; do
 			((fileswc++))
-			tput rc
-			tput el
-			fn_print "gathering ${label} ${allowed_extention} : ${fileswc}"
+			if [ -z "${plainoutput}" ]; then
+				tput rc
+				tput el
+				fn_print "gathering ${label} ${allowed_extention} : ${fileswc}"
+			fi
 			fastdlrelpath="${fastdlfile#"${basedir}/"}"
 			printf '%s\t%s\t%s\t%s\t%s\n' "${priority}" "${fastdlrelpath#./}" "${filesize}" "${filemtime}" "${fastdlfile}" >> "${fastdlfilelist}"
 		done < <(find "${basedir}/${directory}" "${fastdl_find_maxdepth[@]}" -type f -iname "${allowed_extention}" -printf '%s\t%T@\t%p\n')
-		tput rc
-		tput el
+		if [ -z "${plainoutput}" ]; then
+			tput rc
+			tput el
+		fi
 		fn_print "gathering ${label} ${allowed_extention} : ${fileswc}"
 		if [ ${fileswc} != 0 ]; then
 			fn_print_ok_eol_nl
@@ -220,12 +226,16 @@ fn_fastdl_gather_gmod() {
 	allowed_extentions_array=("*.ain" "*.bsp" "*.mdl" "*.mp3" "*.ogg" "*.otf" "*.pcf" "*.phy" "*.png" "*.svg" "*.vtf" "*.vmt" "*.vtx" "*.vvd" "*.ttf" "*.wav")
 	for allowed_extention in "${allowed_extentions_array[@]}"; do
 		fileswc=0
-		tput sc
+		if [ -z "${plainoutput}" ]; then
+			tput sc
+		fi
 		while IFS=$'\t' read -r filesize filemtime fastdlfile; do
 			((fileswc++))
-			tput rc
-			tput el
-			fn_print "gathering ${allowed_extention} : ${fileswc}"
+			if [ -z "${plainoutput}" ]; then
+				tput rc
+				tput el
+				fn_print "gathering ${allowed_extention} : ${fileswc}"
+			fi
 			fastdlrelpath="${fastdlfile#./}"
 			priority="2"
 			if [[ "${fastdlrelpath}" == addons/* ]]; then
@@ -242,6 +252,11 @@ fn_fastdl_gather_gmod() {
 				printf '%s\t%s\t%s\t%s\t%s\n' "0" "${fastdlrelpath#lua/}" "${filesize}" "${filemtime}" "${systemdir}/${fastdlfile#./}" >> "${fastdlfilelist}"
 			fi
 		done < <(find . -type f -iname "${allowed_extention}" -printf '%s\t%T@\t%p\n')
+		if [ -z "${plainoutput}" ]; then
+			tput rc
+			tput el
+		fi
+		fn_print "gathering ${allowed_extention} : ${fileswc}"
 		if [ ${fileswc} != 0 ]; then
 			fn_print_ok_eol_nl
 		else
@@ -434,12 +449,16 @@ fn_fastdl_remove() {
 fn_fastdl_copy() {
 	if [ -s "${fastdlupdatelist}" ]; then
 		fileswc=0
-		tput sc
+		if [ -z "${plainoutput}" ]; then
+			tput sc
+		fi
 		while IFS=$'\t' read -r fastdlmode fastdlrelpath fastdlfile; do
 			((fileswc++))
-			tput rc
-			tput el
-			fn_print "copying files : ${fileswc}/${updatefiles}"
+			if [ -z "${plainoutput}" ]; then
+				tput rc
+				tput el
+				fn_print "copying files : ${fileswc}/${updatefiles}"
+			fi
 			fastdltarget="${fastdldir}/${fastdlrelpath}"
 			mkdir -p "${fastdltarget%/*}" && cp -p "${fastdlfile}" "${fastdltarget}"
 			exitcode=$?
@@ -454,6 +473,9 @@ fn_fastdl_copy() {
 				printf '%s\0' "${fastdltarget}" >> "${fastdlcompresslist}"
 			fi
 		done < "${fastdlupdatelist}"
+		if [ -n "${plainoutput}" ]; then
+			fn_print "copying files : ${fileswc}/${updatefiles}"
+		fi
 		fn_print_ok_eol_nl
 	fi
 }

+ 7 - 3
lgsm/modules/command_mods_remove.sh

@@ -82,7 +82,9 @@ echo -e "* location: ${modinstalldir}"
 fn_sleep_time_1
 # Go through every file and remove it.
 modfileline="1"
-tput sc
+if [ -z "${plainoutput}" ]; then
+	tput sc
+fi
 while [ "${modfileline}" -le "${modsfilelistsize}" ]; do
 	# Current line defines current file to remove.
 	currentfileremove=$(sed "${modfileline}q;d" "${modsdir}/${modcommand}-files.txt")
@@ -98,8 +100,10 @@ while [ "${modfileline}" -le "${modsfilelistsize}" ]; do
 			fn_script_log_pass "Removing ${modinstalldir}/${currentfileremove}"
 		fi
 	fi
-	tput rc
-	tput el
+	if [ -z "${plainoutput}" ]; then
+		tput rc
+		tput el
+	fi
 	echo -e "removing ${modprettyname} ${modfileline} / ${modsfilelistsize} : ${currentfileremove}..."
 	((modfileline++))
 done

+ 15 - 6
lgsm/modules/core_dl.sh

@@ -428,8 +428,12 @@ fn_check_file() {
 				fi
 			fi
 		else
-			fn_print_ok_eol
-			echo -en "\033[2K\\r"
+			if [ -n "${plainoutput}" ]; then
+				fn_print_ok_eol_nl
+			else
+				fn_print_ok_eol
+				echo -en "\033[2K\\r"
+			fi
 			if [ -f "${lgsmlog}" ]; then
 				fn_script_log_pass "Checking ${remote_filename}"
 				checkflag=0
@@ -496,10 +500,15 @@ fn_fetch_file() {
 			local exitcode=""
 			large_files=("bz2" "gz" "zip" "jar" "xz")
 			if grep -qE "(^|\s)${local_filename##*.}(\s|$)" <<< "${large_files[@]}"; then
-				echo -e "downloading file [ ${italic}${local_filename}${default} ]"
-				fn_sleep_time
-				"${curlcmd[@]}" --progress-bar "${fileurl}" 2>&1
-				exitcode=$?
+				if [ -n "${plainoutput}" ]; then
+					"${curlcmd[@]}" --silent --show-error "${fileurl}" 2>&1
+					exitcode=$?
+				else
+					echo -e "downloading file [ ${italic}${local_filename}${default} ]"
+					fn_sleep_time
+					"${curlcmd[@]}" --progress-bar "${fileurl}" 2>&1
+					exitcode=$?
+				fi
 				echo -en "downloading file [ ${italic}${local_filename}${default} ]"
 			else
 				echo -en "fetching ${fileurl_name} [ ${italic}${local_filename}${default} ]\c"

+ 2 - 0
lgsm/modules/core_exit.sh

@@ -19,6 +19,8 @@ fn_exit_dev_debug() {
 	fi
 }
 
+fn_print_plain_flush
+
 # If running dependency check as root will remove any files that belong to root user.
 if [ "$(whoami)" == "root" ]; then
 	find "${lgsmdir}"/ -group root -prune -exec rm -rf {} + > /dev/null 2>&1

+ 87 - 40
lgsm/modules/core_messages.sh

@@ -10,9 +10,16 @@ moduleselfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")"
 # nl: new line: message is following by a new line.
 # eol: end of line: message is placed at the end of the current line.
 fn_ansi_loader() {
+	# plain: stdout is not a terminal, output complete lines with no terminal control sequences.
+	unset plainoutput plainpending
+	if [ ! -t 1 ]; then
+		plainoutput="1"
+	fi
 	# carriage return.
 	creeol="\r"
-	if [ "${ansi}" != "off" ]; then
+	if [ -n "${plainoutput}" ]; then
+		creeol=""
+	elif [ "${ansi}" != "off" ]; then
 		# echo colors
 		default="\e[0m"
 		black="\e[30m"
@@ -33,15 +40,21 @@ fn_ansi_loader() {
 		white="\e[97m"
 		# erase to end of line.
 		creeol+="\033[K"
+		# text styles
+		bold="\e[1m"
+		dim="\e[2m"
+		italic="\e[3m"
+		underline="\e[4m"
+		reverse="\e[7m"
 	fi
-	# carriage return & erase to end of line.
-	creeol="\r\033[K"
+}
 
-	bold="\e[1m"
-	dim="\e[2m"
-	italic="\e[3m"
-	underline="\e[4m"
-	reverse="\e[7m"
+# plain: print any held in-place message as its own line.
+fn_print_plain_flush() {
+	if [ -n "${plainpending}" ]; then
+		echo -e "${plainpending}"
+		unset plainpending
+	fi
 }
 
 fn_sleep_time() {
@@ -110,22 +123,31 @@ fn_script_log_info() {
 ##################################
 
 fn_print() {
+	fn_print_plain_flush
 	echo -en "$*${default}"
 }
 
 fn_print_nl() {
+	fn_print_plain_flush
 	echo -e "$*${default}"
 }
 
 # Helper function to print messages with a specific format and color
+# plain: in-place messages are held until replaced, completed by an eol message or flushed.
 fn_print_message() {
 	local type="$1"
 	local color="$2"
 	local message="$3"
+	local line
 	if [ "${commandaction}" ]; then
-		echo -en "${bold}${creeol}[${color} ${type} ${default}]${default} ${commandaction} ${selfname}: ${message}${default}"
+		line="[${color} ${type} ${default}]${default} ${commandaction} ${selfname}: ${message}${default}"
+	else
+		line="[${color} ${type} ${default}]${default} ${message}${default}"
+	fi
+	if [ -n "${plainoutput}" ]; then
+		plainpending="${line}"
 	else
-		echo -en "${bold}${cree}[${color} ${type} ${default}]${default} ${message}${default}"
+		echo -en "${bold}${creeol}${line}"
 	fi
 	fn_sleep_time
 }
@@ -134,6 +156,7 @@ fn_print_message_nl() {
 	local type="$1"
 	local color="$2"
 	local message="$3"
+	unset plainpending
 	if [ "${commandaction}" ]; then
 		echo -e "${bold}${creeol}[${color} ${type} ${default}]${default} ${commandaction} ${selfname}: ${message}${default}"
 	else
@@ -142,6 +165,28 @@ fn_print_message_nl() {
 	fn_sleep_time
 }
 
+# Helper function to print eol messages with a specific color
+fn_print_eol() {
+	local type="$1"
+	local color="$2"
+	if [ -n "${plainpending}" ]; then
+		plainpending+=" ... ${type}"
+	else
+		echo -en " ... ${color}${type}${default}"
+	fi
+}
+
+fn_print_eol_nl() {
+	local type="$1"
+	local color="$2"
+	if [ -n "${plainpending}" ]; then
+		echo -e "${plainpending} ... ${type}"
+		unset plainpending
+	else
+		echo -e " ... ${color}${type}${default}"
+	fi
+}
+
 # [ .... ]
 fn_print_dots() {
 	fn_print_message "...." "${default}" "$*"
@@ -234,6 +279,7 @@ fn_messages_separator() {
 # No More Room in Hell Debug
 # =================================
 fn_print_header() {
+	fn_print_plain_flush
 	echo -e ""
 	echo -e "${bold}${lightyellow}${gamename} ${commandaction}${default}"
 	fn_messages_separator
@@ -307,6 +353,7 @@ fn_print_skip2_nl() {
 
 # Y/N Prompt
 fn_prompt_yn() {
+	fn_print_plain_flush
 	echo -e ""
 	local prompt="$1"
 	local initial="$2"
@@ -356,166 +403,166 @@ fn_prompt_message() {
 
 # YES
 fn_print_yes_eol() {
-	echo -en " ... ${cyan}YES${default}"
+	fn_print_eol "YES" "${cyan}"
 	fn_sleep_time
 }
 
 fn_print_yes_eol_nl() {
-	echo -e " ... ${cyan}YES${default}"
+	fn_print_eol_nl "YES" "${cyan}"
 	fn_sleep_time
 }
 
 # NO
 fn_print_no_eol() {
-	echo -en " ... ${red}NO${default}"
+	fn_print_eol "NO" "${red}"
 	fn_sleep_time
 }
 
 fn_print_no_eol_nl() {
-	echo -e " ... ${red}NO${default}"
+	fn_print_eol_nl "NO" "${red}"
 	fn_sleep_time
 }
 
 # OK
 fn_print_ok_eol() {
-	echo -en " ... ${green}OK${default}"
+	fn_print_eol "OK" "${green}"
 	fn_sleep_time
 }
 
 fn_print_ok_eol_nl() {
-	echo -e " ... ${green}OK${default}"
+	fn_print_eol_nl "OK" "${green}"
 	fn_sleep_time
 }
 
 # FAIL
 fn_print_fail_eol() {
-	echo -en " ... ${red}FAIL${default}"
+	fn_print_eol "FAIL" "${red}"
 	fn_sleep_time
 }
 
 fn_print_fail_eol_nl() {
-	echo -e " ... ${red}FAIL${default}"
+	fn_print_eol_nl "FAIL" "${red}"
 	fn_sleep_time
 }
 
 # ERROR
 fn_print_error_eol() {
-	echo -en " ... ${red}ERROR${default}"
+	fn_print_eol "ERROR" "${red}"
 	fn_sleep_time
 }
 
 fn_print_error_eol_nl() {
-	echo -e " ... ${red}ERROR${default}"
+	fn_print_eol_nl "ERROR" "${red}"
 	fn_sleep_time
 }
 
 # WAIT
 fn_print_wait_eol() {
-	echo -en " ... ${cyan}WAIT${default}"
+	fn_print_eol "WAIT" "${cyan}"
 	fn_sleep_time
 }
 
 fn_print_wait_eol_nl() {
-	echo -e " ... ${cyan}WAIT${default}"
+	fn_print_eol_nl "WAIT" "${cyan}"
 	fn_sleep_time
 }
 
 # WARN
 fn_print_warn_eol() {
-	echo -en " ... ${lightyellow}WARN${default}"
+	fn_print_eol "WARN" "${lightyellow}"
 	fn_sleep_time
 }
 
 fn_print_warn_eol_nl() {
-	echo -e " ... ${lightyellow}WARN${default}"
+	fn_print_eol_nl "WARN" "${lightyellow}"
 	fn_sleep_time
 }
 
 # INFO
 fn_print_info_eol() {
-	echo -en " ... ${cyan}INFO${default}"
+	fn_print_eol "INFO" "${cyan}"
 	fn_sleep_time
 }
 
 fn_print_info_eol_nl() {
-	echo -e " ... ${cyan}INFO${default}"
+	fn_print_eol_nl "INFO" "${cyan}"
 	fn_sleep_time
 }
 
 # QUERYING
 fn_print_querying_eol() {
-	echo -en " ... ${cyan}QUERYING${default}"
+	fn_print_eol "QUERYING" "${cyan}"
 	fn_sleep_time_1
 }
 
 fn_print_querying_eol_nl() {
-	echo -e " ... ${cyan}QUERYING${default}"
+	fn_print_eol_nl "QUERYING" "${cyan}"
 	fn_sleep_time_1
 }
 
 # CHECKING
 fn_print_checking_eol() {
-	echo -en " ... ${cyan}CHECKING${default}"
+	fn_print_eol "CHECKING" "${cyan}"
 	fn_sleep_time_1
 }
 
 fn_print_checking_eol_nl() {
-	echo -e " ... ${cyan}CHECKING${default}"
+	fn_print_eol_nl "CHECKING" "${cyan}"
 	fn_sleep_time_1
 }
 
 # DELAY
 fn_print_delay_eol() {
-	echo -en " ... ${green}DELAY${default}"
+	fn_print_eol "DELAY" "${green}"
 	fn_sleep_time_1
 }
 
 fn_print_delay_eol_nl() {
-	echo -e " ... ${green}DELAY${default}"
+	fn_print_eol_nl "DELAY" "${green}"
 	fn_sleep_time_1
 }
 
 # CANCELED
 fn_print_canceled_eol() {
-	echo -en " ... ${lightyellow}CANCELED${default}"
+	fn_print_eol "CANCELED" "${lightyellow}"
 	fn_sleep_time_1
 }
 
 fn_print_canceled_eol_nl() {
-	echo -e " ... ${lightyellow}CANCELED${default}"
+	fn_print_eol_nl "CANCELED" "${lightyellow}"
 	fn_sleep_time_1
 }
 
 # REMOVED
 fn_print_removed_eol() {
-	echo -en " ... ${red}REMOVED${default}"
+	fn_print_eol "REMOVED" "${red}"
 	fn_sleep_time_1
 }
 
 fn_print_removed_eol_nl() {
-	echo -e " ... ${red}REMOVED${default}"
+	fn_print_eol_nl "REMOVED" "${red}"
 	fn_sleep_time_1
 }
 
 # UPDATE
 fn_print_update_eol() {
-	echo -en " ... ${lightblue}UPDATE${default}"
+	fn_print_eol "UPDATE" "${lightblue}"
 	fn_sleep_time
 }
 
 fn_print_update_eol_nl() {
-	echo -e " ... ${lightblue}UPDATE${default}"
+	fn_print_eol_nl "UPDATE" "${lightblue}"
 	fn_sleep_time
 }
 
 # SKIP
 fn_print_skip_eol() {
-	echo -en " ... ${cyan}SKIP${default}"
+	fn_print_eol "SKIP" "${cyan}"
 	fn_sleep_time
 }
 
 fn_print_skip_eol_nl() {
-	echo -e " ... ${cyan}SKIP${default}"
+	fn_print_eol_nl "SKIP" "${cyan}"
 	fn_sleep_time
 }