xref: /freebsd/contrib/bc/scripts/functions.sh (revision a970610a3af63b3f4df5b69d91c6b4093a00ed8f)
1#! /bin/sh
2#
3# SPDX-License-Identifier: BSD-2-Clause
4#
5# Copyright (c) 2018-2024 Gavin D. Howard and contributors.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions are met:
9#
10# * Redistributions of source code must retain the above copyright notice, this
11#   list of conditions and the following disclaimer.
12#
13# * Redistributions in binary form must reproduce the above copyright notice,
14#   this list of conditions and the following disclaimer in the documentation
15#   and/or other materials provided with the distribution.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27# POSSIBILITY OF SUCH DAMAGE.
28#
29
30# This script is NOT meant to be run! It is meant to be sourced by other
31# scripts.
32
33# Reads and follows a link until it finds a real file. This is here because the
34# readlink utility is not part of the POSIX standard. Sigh...
35# @param f  The link to find the original file for.
36readlink() {
37
38	_readlink_f="$1"
39	shift
40
41	_readlink_arrow="-> "
42	_readlink_d=$(dirname "$_readlink_f")
43
44	_readlink_lsout=""
45	_readlink_link=""
46
47	_readlink_lsout=$(ls -dl "$_readlink_f")
48	_readlink_link=$(printf '%s' "${_readlink_lsout#*$_readlink_arrow}")
49
50	while [ -z "${_readlink_lsout##*$_readlink_arrow*}" ]; do
51		_readlink_f="$_readlink_d/$_readlink_link"
52		_readlink_d=$(dirname "$_readlink_f")
53		_readlink_lsout=$(ls -dl "$_readlink_f")
54		_readlink_link=$(printf '%s' "${_readlink_lsout#*$_readlink_arrow}")
55	done
56
57	printf '%s' "${_readlink_f##*$_readlink_d/}"
58}
59
60# Quick function for exiting with an error.
61# @param 1  A message to print.
62# @param 2  The exit code to use.
63err_exit() {
64
65	if [ "$#" -ne 2 ]; then
66		printf 'Invalid number of args to err_exit\n'
67		exit 1
68	fi
69
70	printf '%s\n' "$1"
71	exit "$2"
72}
73
74# Function for checking the "d"/"dir" argument of scripts. This function expects
75# a usage() function to exist in the caller.
76# @param 1  The argument to check.
77check_d_arg() {
78
79	if [ "$#" -ne 1 ]; then
80		printf 'Invalid number of args to check_d_arg\n'
81		exit 1
82	fi
83
84	_check_d_arg_arg="$1"
85	shift
86
87	if [ "$_check_d_arg_arg" != "bc" ] && [ "$_check_d_arg_arg" != "dc" ]; then
88		_check_d_arg_msg=$(printf 'Invalid d arg: %s\nMust be either "bc" or "dc".\n\n' \
89			"$_check_d_arg_arg")
90		usage "$_check_d_arg_msg"
91	fi
92}
93
94# Function for checking the boolean arguments of scripts. This function expects
95# a usage() function to exist in the caller.
96# @param 1  The argument to check.
97check_bool_arg() {
98
99	if [ "$#" -ne 1 ]; then
100		printf 'Invalid number of args to check_bool_arg\n'
101		exit 1
102	fi
103
104	_check_bool_arg_arg="$1"
105	shift
106
107	if [ "$_check_bool_arg_arg" != "0" ] && [ "$_check_bool_arg_arg" != "1" ]; then
108		_check_bool_arg_msg=$(printf 'Invalid bool arg: %s\nMust be either "0" or "1".\n\n' \
109			"$_check_bool_arg_arg")
110		usage "$_check_bool_arg_msg"
111	fi
112}
113
114# Function for checking the executable arguments of scripts. This function
115# expects a usage() function to exist in the caller.
116# @param 1  The argument to check.
117check_exec_arg() {
118
119	if [ "$#" -ne 1 ]; then
120		printf 'Invalid number of args to check_exec_arg\n'
121		exit 1
122	fi
123
124	_check_exec_arg_arg="$1"
125	shift
126
127	if [ ! -x "$_check_exec_arg_arg" ]; then
128		if ! command -v "$_check_exec_arg_arg" >/dev/null 2>&1; then
129			_check_exec_arg_msg=$(printf 'Invalid exec arg: %s\nMust be an executable file.\n\n' \
130				"$_check_exec_arg_arg")
131			usage "$_check_exec_arg_msg"
132		fi
133	fi
134}
135
136# Function for checking the file arguments of scripts. This function expects a
137# usage() function to exist in the caller.
138# @param 1  The argument to check.
139check_file_arg() {
140
141	if [ "$#" -ne 1 ]; then
142		printf 'Invalid number of args to check_file_arg\n'
143		exit 1
144	fi
145
146	_check_file_arg_arg="$1"
147	shift
148
149	if [ ! -f "$_check_file_arg_arg" ]; then
150		_check_file_arg_msg=$(printf 'Invalid file arg: %s\nMust be a file.\n\n' \
151			"$_check_file_arg_arg")
152		usage "$_check_file_arg_msg"
153	fi
154}
155
156# Check the return code on a test and exit with a fail if it's non-zero.
157# @param d     The calculator under test.
158# @param err   The return code.
159# @param name  The name of the test.
160checktest_retcode() {
161
162	_checktest_retcode_d="$1"
163	shift
164
165	_checktest_retcode_err="$1"
166	shift
167
168	_checktest_retcode_name="$1"
169	shift
170
171	if [ "$_checktest_retcode_err" -ne 0 ]; then
172		printf 'FAIL!!!\n'
173		err_exit "$_checktest_retcode_d failed test '$_checktest_retcode_name' with error code $_checktest_retcode_err" 1
174	fi
175}
176
177# Check the result of a test. First, it checks the error code using
178# checktest_retcode(). Then it checks the output against the expected output
179# and fails if it doesn't match.
180# @param d             The calculator under test.
181# @param err           The error code.
182# @param name          The name of the test.
183# @param test_path     The path to the test.
184# @param results_name  The path to the file with the expected result.
185checktest() {
186
187	_checktest_d="$1"
188	shift
189
190	_checktest_err="$1"
191	shift
192
193	_checktest_name="$1"
194	shift
195
196	_checktest_test_path="$1"
197	shift
198
199	_checktest_results_name="$1"
200	shift
201
202	checktest_retcode "$_checktest_d" "$_checktest_err" "$_checktest_name"
203
204	_checktest_diff=$(diff "$_checktest_test_path" "$_checktest_results_name")
205
206	_checktest_err="$?"
207
208	if [ "$_checktest_err" -ne 0 ]; then
209		printf 'FAIL!!!\n'
210		printf '%s\n' "$_checktest_diff"
211		err_exit "$_checktest_d failed test $_checktest_name" 1
212	fi
213}
214
215# Die. With a message.
216# @param d     The calculator under test.
217# @param msg   The message to print.
218# @param name  The name of the test.
219# @param err   The return code from the test.
220die() {
221
222	_die_d="$1"
223	shift
224
225	_die_msg="$1"
226	shift
227
228	_die_name="$1"
229	shift
230
231	_die_err="$1"
232	shift
233
234	_die_str=$(printf '\n%s %s on test:\n\n    %s\n' "$_die_d" "$_die_msg" "$_die_name")
235
236	err_exit "$_die_str" "$_die_err"
237}
238
239# Check that a test did not crash and die if it did.
240# @param d      The calculator under test.
241# @param error  The error code.
242# @param name   The name of the test.
243checkcrash() {
244
245	_checkcrash_d="$1"
246	shift
247
248	_checkcrash_error="$1"
249	shift
250
251	_checkcrash_name="$1"
252	shift
253
254
255	if [ "$_checkcrash_error" -gt 127 ]; then
256		die "$_checkcrash_d" "crashed ($_checkcrash_error)" \
257			"$_checkcrash_name" "$_checkcrash_error"
258	fi
259}
260
261# Check that a test had an error or crash.
262# @param d        The calculator under test.
263# @param error    The error code.
264# @param name     The name of the test.
265# @param out      The file that the test results were output to.
266# @param exebase  The name of the executable.
267checkerrtest()
268{
269	_checkerrtest_d="$1"
270	shift
271
272	_checkerrtest_error="$1"
273	shift
274
275	_checkerrtest_name="$1"
276	shift
277
278	_checkerrtest_out="$1"
279	shift
280
281	_checkerrtest_exebase="$1"
282	shift
283
284	checkcrash "$_checkerrtest_d" "$_checkerrtest_error" "$_checkerrtest_name"
285
286	if [ "$_checkerrtest_error" -eq 0 ]; then
287		die "$_checkerrtest_d" "returned no error" "$_checkerrtest_name" 127
288	fi
289
290	# This is to check for memory errors with Valgrind, which is told to return
291	# 100 on memory errors.
292	if [ "$_checkerrtest_error" -eq 100 ]; then
293
294		_checkerrtest_output=$(cat "$_checkerrtest_out")
295		_checkerrtest_fatal_error="Fatal error"
296
297		if [ "${_checkerrtest_output##*$_checkerrtest_fatal_error*}" ]; then
298			printf "%s\n" "$_checkerrtest_output"
299			die "$_checkerrtest_d" "had memory errors on a non-fatal error" \
300				"$_checkerrtest_name" "$_checkerrtest_error"
301		fi
302	fi
303
304	if [ ! -s "$_checkerrtest_out" ]; then
305		die "$_checkerrtest_d" "produced no error message" "$_checkerrtest_name" "$_checkerrtest_error"
306	fi
307
308	# To display error messages, uncomment this line. This is useful when
309	# debugging.
310	#cat "$_checkerrtest_out"
311}
312
313# Replace a substring in a string with another. This function is the *real*
314# workhorse behind configure.sh's generation of a Makefile.
315#
316# This function uses a sed call that uses exclamation points `!` as delimiters.
317# As a result, needle can never contain an exclamation point. Oh well.
318#
319# @param str          The string that will have any of the needle replaced by
320#                     replacement.
321# @param needle       The needle to replace in str with replacement.
322# @param replacement  The replacement for needle in str.
323substring_replace() {
324
325	_substring_replace_str="$1"
326	shift
327
328	_substring_replace_needle="$1"
329	shift
330
331	_substring_replace_replacement="$1"
332	shift
333
334	_substring_replace_result=$(printf '%s\n' "$_substring_replace_str" | \
335		sed -e "s!$_substring_replace_needle!$_substring_replace_replacement!g")
336
337	printf '%s' "$_substring_replace_result"
338}
339
340# Generates an NLS path based on the locale and executable name.
341#
342# This is a monstrosity for a reason.
343#
344# @param nlspath   The $NLSPATH
345# @param locale    The locale.
346# @param execname  The name of the executable.
347gen_nlspath() {
348
349	_gen_nlspath_nlspath="$1"
350	shift
351
352	_gen_nlspath_locale="$1"
353	shift
354
355	_gen_nlspath_execname="$1"
356	shift
357
358	# Split the locale into its modifier and other parts.
359	_gen_nlspath_char="@"
360	_gen_nlspath_modifier="${_gen_nlspath_locale#*$_gen_nlspath_char}"
361	_gen_nlspath_tmplocale="${_gen_nlspath_locale%%$_gen_nlspath_char*}"
362
363	# Split the locale into charset and other parts.
364	_gen_nlspath_char="."
365	_gen_nlspath_charset="${_gen_nlspath_tmplocale#*$_gen_nlspath_char}"
366	_gen_nlspath_tmplocale="${_gen_nlspath_tmplocale%%$_gen_nlspath_char*}"
367
368	# Check for an empty charset.
369	if [ "$_gen_nlspath_charset" = "$_gen_nlspath_tmplocale" ]; then
370		_gen_nlspath_charset=""
371	fi
372
373	# Split the locale into territory and language.
374	_gen_nlspath_char="_"
375	_gen_nlspath_territory="${_gen_nlspath_tmplocale#*$_gen_nlspath_char}"
376	_gen_nlspath_language="${_gen_nlspath_tmplocale%%$_gen_nlspath_char*}"
377
378	# Check for empty territory and language.
379	if [ "$_gen_nlspath_territory" = "$_gen_nlspath_tmplocale" ]; then
380		_gen_nlspath_territory=""
381	fi
382
383	if [ "$_gen_nlspath_language" = "$_gen_nlspath_tmplocale" ]; then
384		_gen_nlspath_language=""
385	fi
386
387	# Prepare to replace the format specifiers. This is done by wrapping the in
388	# pipe characters. It just makes it easier to split them later.
389	_gen_nlspath_needles="%%:%L:%N:%l:%t:%c"
390
391	_gen_nlspath_needles=$(printf '%s' "$_gen_nlspath_needles" | tr ':' '\n')
392
393	for _gen_nlspath_i in $_gen_nlspath_needles; do
394		_gen_nlspath_nlspath=$(substring_replace "$_gen_nlspath_nlspath" "$_gen_nlspath_i" "|$_gen_nlspath_i|")
395	done
396
397	# Replace all the format specifiers.
398	_gen_nlspath_nlspath=$(substring_replace "$_gen_nlspath_nlspath" "%%" "%")
399	_gen_nlspath_nlspath=$(substring_replace "$_gen_nlspath_nlspath" "%L" "$_gen_nlspath_locale")
400	_gen_nlspath_nlspath=$(substring_replace "$_gen_nlspath_nlspath" "%N" "$_gen_nlspath_execname")
401	_gen_nlspath_nlspath=$(substring_replace "$_gen_nlspath_nlspath" "%l" "$_gen_nlspath_language")
402	_gen_nlspath_nlspath=$(substring_replace "$_gen_nlspath_nlspath" "%t" "$_gen_nlspath_territory")
403	_gen_nlspath_nlspath=$(substring_replace "$_gen_nlspath_nlspath" "%c" "$_gen_nlspath_charset")
404
405	# Get rid of pipe characters.
406	_gen_nlspath_nlspath=$(printf '%s' "$_gen_nlspath_nlspath" | tr -d '|')
407
408	# Return the result.
409	printf '%s' "$_gen_nlspath_nlspath"
410}
411
412ALL=0
413NOSKIP=1
414SKIP=2
415
416# Filters text out of a file according to the build type.
417# @param in    File to filter.
418# @param out   File to write the filtered output to.
419# @param type  Build type.
420filter_text() {
421
422	_filter_text_in="$1"
423	shift
424
425	_filter_text_out="$1"
426	shift
427
428	_filter_text_buildtype="$1"
429	shift
430
431	# Set up some local variables.
432	_filter_text_status="$ALL"
433	_filter_text_last_line=""
434
435	# We need to set IFS, so we store it here for restoration later.
436	_filter_text_ifs="$IFS"
437
438	# Remove the file- that will be generated.
439	rm -rf "$_filter_text_out"
440
441	# Here is the magic. This loop reads the template line-by-line, and based on
442	# _filter_text_status, either prints it to the markdown manual or not.
443	#
444	# Here is how the template is set up: it is a normal markdown file except
445	# that there are sections surrounded tags that look like this:
446	#
447	# {{ <build_type_list> }}
448	# ...
449	# {{ end }}
450	#
451	# Those tags mean that whatever build types are found in the
452	# <build_type_list> get to keep that section. Otherwise, skip.
453	#
454	# Obviously, the tag itself and its end are not printed to the markdown
455	# manual.
456	while IFS= read -r _filter_text_line; do
457
458		# If we have found an end, reset the status.
459		if [ "$_filter_text_line" = "{{ end }}" ]; then
460
461			# Some error checking. This helps when editing the templates.
462			if [ "$_filter_text_status" -eq "$ALL" ]; then
463				err_exit "{{ end }} tag without corresponding start tag" 2
464			fi
465
466			_filter_text_status="$ALL"
467
468		# We have found a tag that allows our build type to use it.
469		elif [ "${_filter_text_line#\{\{* $_filter_text_buildtype *\}\}}" != "$_filter_text_line" ]; then
470
471			# More error checking. We don't want tags nested.
472			if [ "$_filter_text_status" -ne "$ALL" ]; then
473				err_exit "start tag nested in start tag" 3
474			fi
475
476			_filter_text_status="$NOSKIP"
477
478		# We have found a tag that is *not* allowed for our build type.
479		elif [ "${_filter_text_line#\{\{*\}\}}" != "$_filter_text_line" ]; then
480
481			if [ "$_filter_text_status" -ne "$ALL" ]; then
482				err_exit "start tag nested in start tag" 3
483			fi
484
485			_filter_text_status="$SKIP"
486
487		# This is for normal lines. If we are not skipping, print.
488		else
489			if [ "$_filter_text_status" -ne "$SKIP" ]; then
490				if [ "$_filter_text_line" != "$_filter_text_last_line" ]; then
491					printf '%s\n' "$_filter_text_line" >> "$_filter_text_out"
492				fi
493				_filter_text_last_line="$_filter_text_line"
494			fi
495		fi
496
497	done < "$_filter_text_in"
498
499	# Reset IFS.
500	IFS="$_filter_text_ifs"
501}
502