• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyberShadow / aconfmgr / 542

14 Nov 2024 08:08PM UTC coverage: 93.559% (+0.04%) from 93.518%
542

push

github

web-flow
Merge pull request #210 from AleksanderBobinski/bugfix/stale_database_fixup

src/apply.bash: Fix optional sys upgrade

4 of 4 new or added lines in 2 files covered. (100.0%)

3 existing lines in 1 file now uncovered.

4590 of 4906 relevant lines covered (93.56%)

469.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.32
/src/common.bash
1
# common.bash
2

3
# This file contains aconfmgr's common code, used by all commands.
4

5
####################################################################################################
6

7
# Globals
8

9
PACMAN=${PACMAN:-pacman}
1,733✔
10

11
output_dir="$tmp_dir"/output
1,733✔
12
system_dir="$tmp_dir"/system # Current system configuration, to be compared against the output directory
1,733✔
13

14
# The directory used for building AUR packages.
15
# When running as root, our "$tmp_dir" will be inaccessible to
16
# the user under which the building is performed ("nobody"),
17
# so we need a separate directory under this circumstance.
18
if [[ $EUID == 0 ]]
1,733✔
19
then
20
        aur_dir="$tmp_dir"-aur
×
21
else
22
        aur_dir="$tmp_dir"/aur
1,733✔
23
fi
24

25
default_file_mode=644
1,733✔
26

27
ANSI_clear_line=" [0K"
1,733✔
28
ANSI_color_R=" [1;31m"
1,733✔
29
ANSI_color_G=" [1;32m"
1,733✔
30
ANSI_color_Y=" [1;33m"
1,733✔
31
ANSI_color_B=" [1;34m"
1,733✔
32
ANSI_color_M=" [1;35m"
1,733✔
33
ANSI_color_C=" [1;36m"
1,733✔
34
ANSI_color_W=" [1;39m"
1,733✔
35
ANSI_reset=" [0m"
1,733✔
36

37
verbose=0
1,733✔
38
lint_config=false
1,733✔
39
umask $((666 - default_file_mode))
1,733✔
40

41
aconfmgr_action=
1,733✔
42
aconfmgr_action_args=()
1,733✔
43

44
####################################################################################################
45

46
# Defaults
47

48
# Initial ignore path list.
49
# Can be appended to using the IgnorePath helper.
50
ignore_paths=(
1,733✔
51
    '/dev'
1,733✔
52
    '/home'
1,733✔
53
    '/media'
1,733✔
54
    '/mnt'
1,733✔
55
    '/proc'
1,733✔
56
    '/root'
1,733✔
57
    '/run'
1,733✔
58
    '/sys'
1,733✔
59
    '/tmp'
1,733✔
60
    # '/var/.updated'
1,733✔
61
    '/var/cache'
1,733✔
62
    # '/var/lib'
1,733✔
63
    # '/var/lock'
1,733✔
64
    # '/var/log'
1,733✔
65
    # '/var/spool'
1,733✔
66
)
1,733✔
67

68
# These files must be installed before anything else,
69
# because they affect or are required for what follows.
70
# shellcheck disable=SC2034
71
priority_files=(
1,733✔
72
        /etc/passwd
1,733✔
73
        /etc/group
1,733✔
74
        /etc/pacman.conf
1,733✔
75
        /etc/pacman.d/mirrorlist
1,733✔
76
        /etc/makepkg.conf
1,733✔
77
)
1,733✔
78

79
# File content filters
80
# These are useful for files which contain some unpredictable element,
81
# such as a timestamp, which can't be considered as part of the system
82
# configuration, and can be safely omitted from the file.
83
# This is an associative array of patterns mapping to function
84
# names. The function is called with the file name as the only
85
# parameter, the file contents on its stdin, and is expected to
86
# provide the filtered contents on its stdout.
87
declare -A file_content_filters
1,733✔
88

89
# Some limits for common-sense warnings.
90
# Feel free to override these in your configuration.
91
warn_size_threshold=$((10*1024*1024)) # Warn on copying files bigger than this
1,733✔
92
warn_file_count_threshold=1000        # Warn on finding this many stray files
1,733✔
93
warn_tmp_df_threshold=$((1024*1024))  # Warn on error if free space in $tmp_dir is below this
1,733✔
94

95
makepkg_user=nobody # when running as root
1,733✔
96

97
####################################################################################################
98

99
function LogLeaveDirStats() {
100
        local dir="$1"
306✔
101
        Log 'Finalizing...\r'
306✔
102
        LogLeave 'Done (%s native packages, %s foreign packages, %s files).\n'        \
2,447✔
103
                         "$(Color G "$(wc -l < "$dir"/packages.txt)")"                                        \
×
104
                         "$(Color G "$(wc -l < "$dir"/foreign-packages.txt)")"                        \
×
105
                         "$(Color G "$(find "$dir"/files -not -type d | wc -l)")"
×
106
}
107

108
skip_config=n
1,733✔
109

110
# Run user configuration scripts, to collect desired state into #output_dir
111
function AconfCompileOutput() {
112
        LogEnter 'Compiling user configuration...\n'
168✔
113

114
        if [[ $skip_config == y ]]
168✔
115
        then
116
                LogLeave 'Skipped.\n'
2✔
117
                return
2✔
118
        fi
119

120
        # shellcheck disable=SC2174
121
        mkdir --mode=700 --parents "$tmp_dir"
166✔
122

123
        rm -rf "$output_dir"
166✔
124
        mkdir --parents "$output_dir"
166✔
125
        mkdir "$output_dir"/files
166✔
126
        touch "$output_dir"/packages.txt
166✔
127
        touch "$output_dir"/foreign-packages.txt
166✔
128
        touch "$output_dir"/file-props.txt
166✔
129
        touch "$output_dir"/warnings
166✔
130
        # shellcheck disable=SC2174
131
        mkdir --mode=700 --parents "$config_dir"
166✔
132

133
        # Configuration
134

135
        Log 'Using configuration in %s\n' "$(Color C "%q" "$config_dir")"
332✔
136

137
        typeset -ag ignore_packages=()
332✔
138
        typeset -ag ignore_foreign_packages=()
332✔
139
        typeset -Ag used_files
166✔
140

141
        local found=n
166✔
142
        local file
166✔
143
        for file in "$config_dir"/*.sh
244✔
144
        do
145
                if [[ -e "$file" ]]
244✔
146
                then
147
                        LogEnter 'Sourcing %s...\n' "$(Color C "%q" "$file")"
452✔
148
                        # shellcheck source=/dev/null
149
                        source "$file"
226✔
150
                        found=y
226✔
151
                        LogLeave ''
225✔
152
                fi
153
        done
154

155
        if $lint_config
166✔
156
        then
157
                # Check for unused files (files not referenced by the CopyFile
158
                # helper).
159
                # Only do this in the "check" action, as unused files do not
160
                # necessarily indicate a bug in the configuration - they may
161
                # simply be used under certain conditions.
162
                if [[ -d "$config_dir"/files ]]
6✔
163
                then
164
                        local line
4✔
165
                        find "$config_dir"/files -type f -print0 | \
4✔
166
                                while read -r -d $'\0' line
8✔
167
                                do
168
                                        local key=${line#"$config_dir"/files}
4✔
169
                                        if [[ -z "${used_files[$key]+x}" ]]
4✔
170
                                        then
171
                                                ConfigWarning 'Unused file: %s\n' \
4✔
172
                                                                          "$(Color C "%q" "$line")"
4✔
173
                                        fi
174
                                done
175
                fi
176
        fi
177

178
        if [[ $found == y ]]
166✔
179
        then
180
                LogLeaveDirStats "$output_dir"
148✔
181
        else
182
                LogLeave 'Done (configuration not found).\n'
18✔
183
        fi
184
}
185

186
skip_inspection=n
1,733✔
187
skip_checksums=n
1,733✔
188

189
# Given a list of paths to ignore (which can contain shell patterns),
190
# creates the list of arguments to give to 'find' to ignore them.
191
# The result is stored in the variable given by name as the first argument.
192
function AconfCreateFindIgnoreArgs() {
193
        # A correct and simple implementation of this function would just give
194
        # each argument as a parameter to find's -wholename, e.g. result in
195
        # (-wholename path1 -o -wholename path2 -o ... -o -wholename pathn)
196
        # However, this is painfully slow if the number of ignore patterns is large
197
        # Instead, this function (ab)uses the fact that if the number of patterns
198
        # given to GNU find is big, then as an implementation detail, using regular
199
        # expressions (-regex option and similar) scales much better than using
200
        # shell patterns (-wholename option and similar)
201
        local ignore_args_varname=$1
158✔
202
        local -n ignore_args_var=$ignore_args_varname
158✔
203
        shift
158✔
204
        local ignore_paths=("$@")
316✔
205

206
        # Divide the ignore paths as simple (contain obviously literal ASCII
207
        # characters or an asterisk wildcard) or complex (such as those using
208
        # character ranges, question mark wildcards, international characters, etc.)
209
        # Simple ignore paths are later going to be converted to regex,
210
        # complex ignore paths are passed literally to find's -wholename
211
        local simple_ignore_paths=()
316✔
212

213
        local ignore_path
158✔
214
        for ignore_path in "${ignore_paths[@]}"
2,688✔
215
        do
216
                # In this regular expression, we don't use ranges like "a-z", since this
217
                # can match non-ASCII characters. Also, the hyphen is at the end of the
218
                # regular expression to avoid it being interpreted as a range
219
                if [[ "$ignore_path" =~ [^abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/\ .*-] ]]
2,688✔
220
                then
221
                        ignore_args_var+=(-wholename "$ignore_path" -o)
10✔
222
                else
223
                        simple_ignore_paths+=("${ignore_path}")
2,678✔
224
                fi
225
        done
226

227
        if [ ${#simple_ignore_paths[@]} -ne 0 ]
158✔
228
        then
229
                # Converting the simple ignore paths to regular expressions just needs to
230
                # handle the asterisk wildcard, and escape a few characters
231
                local ignore_regexps
158✔
232
                echo -n "${simple_ignore_paths[*]}" | \
158✔
233
                        sed 's|[^*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/ ]|[&]|g; s|\*|.*|g' | \
158✔
234
                        mapfile -t ignore_regexps
158✔
235

236
                ignore_args_var+=(-regex "$( IFS='|' ; echo "${ignore_regexps[*]}" )" -o)
474✔
237
        fi
238

239
        ignore_args_var+=(-false)
158✔
240
}
241

242
# Collect system state into $system_dir
243
function AconfCompileSystem() {
244
        LogEnter 'Inspecting system state...\n'
160✔
245

246
        if [[ $skip_inspection == y ]]
160✔
247
        then
248
                LogLeave 'Skipped.\n'
2✔
249
                return
2✔
250
        fi
251

252
        # shellcheck disable=SC2174
253
        mkdir --mode=700 --parents "$tmp_dir"
158✔
254

255
        rm -rf "$system_dir"
158✔
256
        mkdir --parents "$system_dir"
158✔
257
        mkdir "$system_dir"/files
158✔
258
        touch "$system_dir"/file-props.txt
158✔
259
        touch "$system_dir"/orig-file-props.txt
158✔
260

261
        ### Packages
262

263
        LogEnter 'Querying package list...\n'
158✔
264
        ( "$PACMAN" --query --quiet --explicit --native  || true ) | sort | ( grep -vFxf <(PrintArray ignore_packages        ) || true ) > "$system_dir"/packages.txt
728✔
265
        ( "$PACMAN" --query --quiet --explicit --foreign || true ) | sort | ( grep -vFxf <(PrintArray ignore_foreign_packages) || true ) > "$system_dir"/foreign-packages.txt
925✔
266
        LogLeave
158✔
267

268
        ### Files
269

270
        local -a found_files
158✔
271
        found_files=()
158✔
272

273
        # whether the contents is different from the original
274
        local -A found_file_edited
158✔
275
        found_file_edited=()
158✔
276

277
        # Stray files
278

279
        local -a ignore_args
158✔
280
        AconfCreateFindIgnoreArgs ignore_args "${ignore_paths[@]}"
158✔
281

282
        LogEnter 'Enumerating owned files...\n'
158✔
283
        mkdir --parents "$tmp_dir"
158✔
284
        ( "$PACMAN" --query --list --quiet || true ) | sed 's#\/$##' | sort --unique > "$tmp_dir"/owned-files
474✔
285
        LogLeave
158✔
286

287
        LogEnter 'Searching for stray files...\n'
158✔
288

289
        local line
158✔
290
        local -Ag ignored_dirs
158✔
291

292
        AconfNeedProgram gawk gawk n
158✔
293

294
        # Progress display - only show file names once per second
295
        local progress_fd
158✔
296
        exec {progress_fd}> \
158✔
297
                 >( gawk '
158✔
298
BEGIN {
×
299
    RS = "\0";
×
300
    t = systime();
×
301
};
×
302
{
303
    u = systime();
×
304
    if (t != u) {
×
305
        t = u;
×
306
        printf "%s\0", $0;
×
307
        system(""); # https://unix.stackexchange.com/a/83853/4830
×
308
        }
316✔
UNCOV
309
}' | \
×
UNCOV
310
                                while read -r -d $'\0' line
×
311
                                do
312
                                        local path=${line:1}
×
313
                                        path=${path%/*} # Never show files, only directories
×
UNCOV
314
                                        while [[ ${#path} -gt 40 ]]
×
315
                                        do
316
                                                path=${path%/*}
×
317
                                        done
318
                                        Log 'Scanning %s...\r' "$(Color M "%q" "$path")"
×
319
                                done
320
                  )
×
321

322
        local stray_file_count=0
158✔
323
        (
324
                # NB: Regular expressions can be generated by AconfCreateFindIgnoreArgs
325
                #     The posix-extended regex type is used since it's easier to work
326
                #     with (e.g. no need to escape '|' alternations, parenthesis, etc.)
327
                sudo find /                                                                        \
158✔
328
                         -regextype posix-extended                                \
×
329
                         -not                                                                        \
×
330
                         \(                                                                                \
×
331
                                 \(                                                                        \
×
332
                                        "${ignore_args[@]}"                                \
×
333
                                \)                                                                        \
×
334
                                -printf 'I' -print0 -prune                        \
×
335
                        \)                                                                                \
×
336
                        -printf 'O' -print0                                                \
×
337
                        | tee /dev/fd/$progress_fd                                \
158✔
338
                        | ( grep                                                                \
×
339
                                        --null --null-data                                \
316✔
340
                                        --invert-match                                        \
×
341
                                        --fixed-strings                                        \
×
342
                                        --line-regexp                                        \
×
343
                                        --file                                                        \
×
344
                                        <( < "$tmp_dir"/owned-files                \
×
345
                                                 sed -e 's#^#O#'                        \
×
346
                                         )                                                                \
×
347
                                        || true )                                                \
×
348
        ) |                                                                                                \
×
349
                while read -r -d $'\0' line
4,475✔
350
                do
351
                        local file action
4,490✔
352
                        file=${line:1}
4,489✔
353
                        action=${line:0:1}
4,500✔
354

355
                        case "$action" in
4,348✔
356
                                O) # Stray file
357
                                        #echo "ignore_paths+='$file' # "
358
                                        if ((verbose))
295✔
359
                                        then
360
                                                Log '%s\r' "$(Color C "%q" "$file")"
×
361
                                        fi
362

363
                                        found_files+=("$file")
297✔
364
                                        found_file_edited[$file]=y
296✔
365
                                        stray_file_count=$((stray_file_count+1))
295✔
366

367
                                        if [[ $stray_file_count -eq $warn_file_count_threshold ]]
293✔
368
                                        then
369
                                                LogEnter '%s: reached %s stray files while in directory %s.\n' \
×
370
                                                        "$(Color Y "Warning")" \
×
371
                                                        "$(Color G "$stray_file_count")" \
×
372
                                                        "$(Color C "%q" "$(dirname "$file")")"
×
373
                                                LogLeave 'Perhaps add %s (or a parent directory) to configuration to ignore it.\n' \
×
374
                                                                 "$(Color Y "IgnorePath %q" "$(dirname "$file")"/'*')"
×
375
                                                warn_file_count_threshold=$((warn_file_count_threshold * 10))
×
376
                                        fi
377
                                        ;;
378
                                I) # Ignored
379

380
                                        # For convenience, we want to also ignore
381
                                        # directories which contain only ignored files.
382
                                        #
383
                                        # This is so that a rule such as:
384
                                        #
385
                                        # IgnorePath '/foo/bar/baz/*.log'
386
                                        #
387
                                        # does not cause `aconfmgr save` to still emit lines like
388
                                        #
389
                                        # CreateDir /foo/bar
390
                                        # CreateDir /foo/bar/baz
391
                                        #
392
                                        # However, we can't simply exclude parent dirs of
393
                                        # excluded files from the file list, as then they
394
                                        # will show up as missing in the diff against the
395
                                        # compiled configuration. So, later we remove
396
                                        # parent directories of any found un-ignored
397
                                        # files.
398

399
                                        local path="$file"
4,053✔
400
                                        while [[ -n "$path" ]]
11,792✔
401
                                        do
402
                                                ignored_dirs[$path]=y
7,671✔
403
                                                path=${path%/*}
7,647✔
404
                                        done
405
                                        ;;
406
                        esac
407
                done
408

409
        LogLeave 'Done (%s stray files).\n' "$(Color G %s $stray_file_count)"
250✔
410

411
        exec {progress_fd}<&-
145✔
412

413
        LogEnter 'Cleaning up ignored files'\'' directories...\n'
146✔
414

415
        local file
148✔
416
        for file in "${found_files[@]}"
312✔
417
        do
418
                if [[ -z "${ignored_dirs[$file]+x}" ]]
287✔
419
                then
420
                        local path="$file"
175✔
421
                        while [[ -n "$path" ]]
357✔
422
                        do
423
                                unset "ignored_dirs[\$path]"
188✔
424
                                path=${path%/*}
188✔
425
                        done
426
                fi
427
        done
428

429
        LogLeave
92✔
430

431
        # Modified files
432

433
        LogEnter 'Searching for modified files...\n'
91✔
434

435
        AconfNeedProgram paccheck pacutils n
84✔
436
        AconfNeedProgram unbuffer expect n
88✔
437
        local modified_file_count=0
101✔
438
        local -A saw_file
102✔
439

440
        # Tentative tracking of original file properties.
441
        # The canonical version is read from orig-file-props.txt in AconfAnalyzeFiles
442
        unset orig_file_props ; typeset -Ag orig_file_props
202✔
443

444
        : > "$tmp_dir"/file-owners
104✔
445

446
        local paccheck_opts=(unbuffer paccheck --files --file-properties --backup --noupgrade)
221✔
447
        if [[ $skip_checksums == n ]]
113✔
448
        then
449
                paccheck_opts+=(--md5sum)
108✔
450
        fi
451

452
        sudo sh -c "LC_ALL=C stdbuf -o0 $(printf ' %q' "${paccheck_opts[@]}") 2>&1 || true" | \
302✔
453
                while read -r line
68,088✔
454
                do
455
                        if [[ $line =~ ^(.*):\ \'(.*)\'\ (type|size|modification\ time|md5sum|UID|GID|permission|symlink\ target)\ mismatch\ \(expected\ (.*)\)$ ]]
68,034✔
456
                        then
457
                                local package="${BASH_REMATCH[1]}"
2,537✔
458
                                local file="${BASH_REMATCH[2]}"
2,554✔
459
                                local kind="${BASH_REMATCH[3]}"
2,550✔
460
                                local value="${BASH_REMATCH[4]}"
2,553✔
461

462
                                local ignored=n
2,556✔
463
                                local ignore_path
2,561✔
464
                                for ignore_path in "${ignore_paths[@]}"
45,830✔
465
                                do
466
                                        # shellcheck disable=SC2053
467
                                        if [[ "$file" == $ignore_path ]]
45,694✔
468
                                        then
469
                                                ignored=y
1,963✔
470
                                                break
1,957✔
471
                                        fi
472
                                done
473

474
                                if [[ $ignored == n ]]
2,083✔
475
                                then
476
                                        if [[ -z "${saw_file[$file]+x}" ]]
118✔
477
                                        then
478
                                                saw_file[$file]=y
47✔
479
                                                Log '%s: %s\n' "$(Color M "%q" "$package")" "$(Color C "%q" "$file")"
188✔
480
                                                found_files+=("$file")
68✔
481
                                                modified_file_count=$((modified_file_count+1))
68✔
482
                                        fi
483

484
                                        local prop
140✔
485
                                        case "$kind" in
141✔
486
                                                UID)
487
                                                        prop=owner
21✔
488
                                                        value=${value#*/}
21✔
489
                                                        ;;
490
                                                GID)
491
                                                        prop=group
17✔
492
                                                        value=${value#*/}
17✔
493
                                                        ;;
494
                                                permission)
495
                                                        prop=mode
28✔
496
                                                        ;;
497
                                                type|size|modification\ time|md5sum|symlink\ target)
498
                                                        prop=
75✔
499
                                                        found_file_edited[$file]=y
75✔
500
                                                        ;;
501
                                                *)
502
                                                        prop=
×
503
                                                        ;;
504
                                        esac
505

506
                                        if [[ -n "$prop" ]]
141✔
507
                                        then
508
                                                local key="$file:$prop"
66✔
509
                                                orig_file_props[$key]=$value
67✔
510

511
                                                printf '%s\t%s\t%q\n' "$prop" "$value" "$file" >> "$system_dir"/orig-file-props.txt
67✔
512
                                        fi
513
                                fi
514
                                printf '%s\0%s\0' "$file" "$package" >> "$tmp_dir"/file-owners
2,103✔
515
                        elif [[ $line =~ ^(.*):\ \'(.*)\'\ missing\ file$ ]]
65,722✔
516
                        then
517
                                local package="${BASH_REMATCH[1]}"
21✔
518
                                local file="${BASH_REMATCH[2]}"
21✔
519

520
                                local ignored=n
20✔
521
                                local ignore_path
20✔
522
                                for ignore_path in "${ignore_paths[@]}"
345✔
523
                                do
524
                                        # shellcheck disable=SC2053
525
                                        if [[ "$file" == $ignore_path ]]
343✔
526
                                        then
527
                                                ignored=y
3✔
528
                                                break
3✔
529
                                        fi
530
                                done
531

532
                                if [[ $ignored == y ]]
14✔
533
                                then
534
                                        continue
3✔
535
                                fi
536

537
                                Log '%s (missing)...\r' "$(Color M "%q" "$package")"
30✔
538
                                printf '%s\t%s\t%q\n' "deleted" "y" "$file" >> "$system_dir"/file-props.txt
17✔
539
                                printf '%s\0%s\0' "$file" "$package" >> "$tmp_dir"/file-owners
17✔
540
                        elif [[ $line =~ ^warning:\ (.*):\ \'(.*)\'\ read\ error\ \(No\ such\ file\ or\ directory\)$ ]]
65,689✔
541
                        then
542
                                local package="${BASH_REMATCH[1]}"
216✔
543
                                local file="${BASH_REMATCH[2]}"
216✔
544
                                # Ignore
545
                        elif [[ $line =~ ^(.*):\ all\ files\ match\ (database|mtree|mtree\ md5sums)$ ]]
65,476✔
546
                        then
547
                                local package="${BASH_REMATCH[1]}"
65,497✔
548
                                Log '%s...\r' "$(Color M "%q" "$package")"
131,136✔
549
                                #echo "Now at ${BASH_REMATCH[1]}"
550
                        else
551
                                Log 'Unknown paccheck output line: %s\n' "$(Color Y "%q" "$line")"
10✔
552
                        fi
553
                done
554
        LogLeave 'Done (%s modified files).\n' "$(Color G %s $modified_file_count)"
316✔
555

556
        LogEnter 'Reading file attributes...\n'
158✔
557

558
        typeset -a found_file_types found_file_sizes found_file_modes found_file_owners found_file_groups
158✔
559
        if [[ ${#found_files[*]} == 0 ]]
158✔
560
        then
561
                Log 'No files found, skipping.\n'
×
562
        else
563
                Log 'Reading file types...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%F | mapfile -t  found_file_types
632✔
564
                Log 'Reading file sizes...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%s | mapfile -t  found_file_sizes
632✔
565
                Log 'Reading file modes...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%a | mapfile -t  found_file_modes
632✔
566
                Log 'Reading file owners...\n' ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%U | mapfile -t found_file_owners
632✔
567
                Log 'Reading file groups...\n' ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%G | mapfile -t found_file_groups
632✔
568
        fi
569

570
        LogLeave # Reading file attributes
158✔
571

572
        LogEnter 'Checking disk space...\n'
158✔
573
        local -i i
158✔
574
        local -i total_blocks=0
158✔
575
        local -i tmp_block_size tmp_blocks_free
158✔
576
        tmp_block_size=$(stat -f -c %S "$system_dir")
316✔
577
        tmp_blocks_free=$(stat -f -c %f "$system_dir")
316✔
578
        local tmp_fs_type
158✔
579
        tmp_fs_type=$(stat -f -c %T "$system_dir")
316✔
580
        if [[ "$tmp_fs_type" != "ramfs" ]]
158✔
581
        then
582
                for ((i=0; i<${#found_files[*]}; i++))
1,502✔
583
                do
584
                        local -i size="${found_file_sizes[$i]}"
593✔
585
                        local -i blocks=$(((size+tmp_block_size-1)/tmp_block_size)) # Count blocks
593✔
586
                        total_blocks+=$blocks
593✔
587
                        if (( total_blocks >= tmp_blocks_free ))
593✔
588
                        then
589
                                local file="${found_files[$i]}"
×
590
                                Log 'Copying file %s (%s bytes / %s blocks) to temporary storage would exhaust free space on %s (%s bytes / %s blocks).\n' \
×
591
                                        "$(Color C "%q" "$file")" "$(Color G "$size")" "$(Color G "$blocks")" \
×
592
                                        "$(Color C "%q" "$system_dir")" "$(Color G "$((tmp_blocks_free * tmp_block_size))")" "$(Color G "$tmp_blocks_free")"
×
593
                                Log 'Perhaps add %s (or a parent directory) to configuration to ignore it, or run with %s pointing at another location.\n' \
×
594
                                        "$(Color Y "IgnorePath %q" "$(dirname "$file")"/'*')" "$(Color Y "TMPDIR")"
×
595
                                FatalError 'Refusing to proceed.\n'
×
596
                        fi
597
                done
598
        fi
599
        LogLeave
158✔
600

601
        LogEnter 'Processing found files...\n'
158✔
602

603
        for ((i=0; i<${#found_files[*]}; i++))
1,502✔
604
        do
605
                Log '%s/%s...\r' "$(Color G "$i")" "$(Color G "${#found_files[*]}")"
1,779✔
606

607
                local  file="${found_files[$i]}"
593✔
608
                local  type="${found_file_types[$i]}"
593✔
609
                local  size="${found_file_sizes[$i]}"
593✔
610
                local  mode="${found_file_modes[$i]}"
593✔
611
                local owner="${found_file_owners[$i]}"
593✔
612
                local group="${found_file_groups[$i]}"
593✔
613

614
                if [[ "${ignored_dirs[$file]-n}" == y ]]
593✔
615
                then
616
                        continue
291✔
617
                fi
618

619
                if [[ -n "${found_file_edited[$file]+x}" ]]
302✔
620
                then
621
                        mkdir --parents "$(dirname "$system_dir"/files/"$file")"
592✔
622
                        if [[ "$type" == "symbolic link" ]]
296✔
623
                        then
624
                                ln -s -- "$(sudo readlink "$file")" "$system_dir"/files/"$file"
72✔
625
                        elif [[ "$type" == "regular file" || "$type" == "regular empty file" ]]
456✔
626
                        then
627
                                if [[ $size -gt $warn_size_threshold ]]
68✔
628
                                then
629
                                        Log '%s: copying large file %s (%s bytes). Add %s to configuration to ignore.\n' "$(Color Y "Warning")" "$(Color C "%q" "$file")" "$(Color G "$size")" "$(Color Y "IgnorePath %q" "$file")"
×
630
                                fi
631

632
                                local filter_pattern filter_func
68✔
633
                                unset filter_func
68✔
634
                                for filter_pattern in "${!file_content_filters[@]}"
6✔
635
                                do
636
                                        # shellcheck disable=SC2053
637
                                        if [[ "$file" == $filter_pattern ]]
6✔
638
                                        then
639
                                                filter_func=${file_content_filters[$filter_pattern]}
6✔
640
                                        fi
641
                                done
642

643
                                if [[ -v filter_func ]]
68✔
644
                                then
645
                                        sudo cat "$file" | "$filter_func" "$file" > "$system_dir"/files/"$file"
12✔
646
                                else
647
                                        # shellcheck disable=SC2024
648
                                        sudo cat "$file" > "$system_dir"/files/"$file"
62✔
649
                                fi
650
                        elif [[ "$type" == "directory" ]]
192✔
651
                        then
652
                                mkdir --parents "$system_dir"/files/"$file"
192✔
653
                        else
654
                                Log '%s: Skipping file %s with unknown type %s. Add %s to configuration to ignore.\n' "$(Color Y "Warning")" "$(Color C "%q" "$file")" "$(Color G "$type")" "$(Color Y "IgnorePath %q" "$file")"
×
655
                                continue
×
656
                        fi
657
                fi
658

659
                {
660
                        local prop
302✔
661
                        for prop in mode owner group
906✔
662
                        do
663
                                # Ignore mode "changes" in symbolic links
664
                                # If a file's type changes, a change in mode can be reported too.
665
                                # But, symbolic links cannot have a mode, so ignore this change.
666
                                if [[ "$type" == "symbolic link" && "$prop" == mode ]]
1,014✔
667
                                then
668
                                        continue
36✔
669
                                fi
670

671
                                local value
870✔
672
                                eval "value=\$$prop"
1,740✔
673

674
                                local default_value
870✔
675

676
                                if [[ $i -lt $stray_file_count ]]
870✔
677
                                then
678
                                        # For stray files, the default owner/group is root/root,
679
                                        # and the default mode depends on the type.
680
                                        # Let AconfDefaultFileProp get the correct default value for us.
681

682
                                        default_value=
696✔
683
                                else
684
                                        # For owned files, we assume that the defaults are the
685
                                        # files' current properties, unless paccheck said
686
                                        # otherwise.
687

688
                                        default_value=$value
174✔
689
                                fi
690

691
                                local orig_value
870✔
692
                                orig_value=$(AconfDefaultFileProp "$file" "$prop" "$type" "$default_value")
1,740✔
693

694
                                [[ "$value" == "$orig_value" ]] || printf '%s\t%s\t%q\n' "$prop" "$value" "$file"
992✔
695
                        done
696
                } >> "$system_dir"/file-props.txt
×
697
        done
698

699
        LogLeave # Processing found files
158✔
700

701
        LogLeaveDirStats "$system_dir" # Inspecting system state
158✔
702
}
703

704
####################################################################################################
705

706
typeset -A file_property_kind_exists
1,733✔
707

708
# Print to stdout the original/default value of the given file property.
709
# Uses orig_file_props entry if present.
710
function AconfDefaultFileProp() {
711
        local file=$1 # Absolute path to the file
894✔
712
        local prop=$2 # Name of the property (owner, group, or mode)
894✔
713
        local type="${3:-}" # Type of the file, as identified by `stat --format=%F`
894✔
714
        local default="${4:-}" # Default value, returned if we don't know the original file property.
894✔
715

716
        local key="$file:$prop"
894✔
717

718
        if [[ -n "${orig_file_props[$key]+x}" ]]
894✔
719
        then
720
                printf '%s' "${orig_file_props[$key]}"
90✔
721
                return
90✔
722
        fi
723

724
        if [[ -n "$default" ]]
804✔
725
        then
726
                printf '%s' "$default"
90✔
727
                return
90✔
728
        fi
729

730
        case "$prop" in
714✔
731
                mode)
732
                        if [[ -z "$type" ]]
234✔
733
                        then
734
                                type=$(sudo env LC_ALL=C stat --format=%F "$file")
12✔
735
                        fi
736

737
                        if [[ "$type" == "symbolic link" ]]
234✔
738
                        then
739
                                FatalError 'Symbolic links do not have a mode\n' # Bug
×
740
                        elif [[ "$type" == "directory" ]]
234✔
741
                        then
742
                                printf 755
178✔
743
                        else
744
                                printf '%s' "$default_file_mode"
56✔
745
                        fi
746
                        ;;
747
                owner|group)
748
                        printf 'root'
480✔
749
                        ;;
750
        esac
751
}
752

753
# Read a file-props.txt file into an associative array.
754
function AconfReadFileProps() {
755
        local filename="$1" # Path to file-props.txt to be read
462✔
756
        local varname="$2"  # Name of global associative array variable to read into
462✔
757

758
        local line
462✔
759
        while read -r line
1,408✔
760
        do
761
                if [[ $line =~ ^(.*)\        (.*)\        (.*)$ ]]
946✔
762
                then
763
                        local kind="${BASH_REMATCH[1]}"
946✔
764
                        local value="${BASH_REMATCH[2]}"
946✔
765
                        local file="${BASH_REMATCH[3]}"
946✔
766
                        file="$(eval "printf %s $file")" # Unescape
2,838✔
767

768
                        if [[ -z "$value" ]]
946✔
769
                        then
770
                                unset "${varname}[\$file:\$kind]"
238✔
771
                        else
772
                                eval "${varname}[\$file:\$kind]=\"\$value\""
1,416✔
773
                        fi
774

775
                        file_property_kind_exists[$kind]=y
946✔
776
                fi
777
        done < "$filename"
×
778
}
779

780
# Compare file properties.
781
function AconfCompareFileProps() {
782
        LogEnter 'Comparing file properties...\n'
306✔
783

784
        typeset -ag system_only_file_props=()
610✔
785
        typeset -ag changed_file_props=()
609✔
786
        typeset -ag config_only_file_props=()
608✔
787

788
        local key
304✔
789
        for key in "${!system_file_props[@]}"
276✔
790
        do
791
                if [[ -z "${output_file_props[$key]+x}" ]]
276✔
792
                then
793
                        system_only_file_props+=("$key")
70✔
794
                fi
795
        done
796

797
        for key in "${!system_file_props[@]}"
276✔
798
        do
799
                if [[ -n "${output_file_props[$key]+x}" && "${system_file_props[$key]}" != "${output_file_props[$key]}" ]]
482✔
800
                then
801
                        changed_file_props+=("$key")
58✔
802
                fi
803
        done
804

805
        for key in "${!output_file_props[@]}"
338✔
806
        do
807
                if [[ -z "${system_file_props[$key]+x}" ]]
338✔
808
                then
809
                        config_only_file_props+=("$key")
145✔
810
                fi
811
        done
812

813
        LogLeave
302✔
814
}
815

816
# Compare file information in $output_dir and $system_dir.
817
function AconfAnalyzeFiles() {
818

819
        #
820
        # Stray/modified files - diff
821
        #
822

823
        LogEnter 'Examining files...\n'
154✔
824

825
        LogEnter 'Loading data...\n'
154✔
826
        mkdir --parents "$tmp_dir"
154✔
827
        ( cd "$output_dir"/files && find . -mindepth 1 -print0 ) | cut --zero-terminated -c 2- | sort --zero-terminated > "$tmp_dir"/output-files
616✔
828
        ( cd "$system_dir"/files && find . -mindepth 1 -print0 ) | cut --zero-terminated -c 2- | sort --zero-terminated > "$tmp_dir"/system-files
616✔
829
        LogLeave
154✔
830

831
        Log 'Comparing file data...\n'
154✔
832

833
        typeset -ag system_only_files=()
308✔
834
        local file
154✔
835

836
        ( comm -13 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
154✔
837
                while read -r -d $'\0' file
224✔
838
                do
839
                        Log 'Only in system: %s\n' "$(Color C "%q" "$file")"
140✔
840
                        system_only_files+=("$file")
70✔
841
                done
842

843
        typeset -ag changed_files=()
308✔
844

845
        AconfNeedProgram diff diffutils n
154✔
846

847
        ( comm -12 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
154✔
848
                while read -r -d $'\0' file
258✔
849
                do
850
                        local output_type system_type
104✔
851
                        output_type=$(LC_ALL=C stat --format=%F "$output_dir"/files/"$file")
312✔
852
                        system_type=$(LC_ALL=C stat --format=%F "$system_dir"/files/"$file")
312✔
853

854
                        if [[ "$output_type" != "$system_type" ]]
104✔
855
                        then
856
                                Log 'Changed type (%s / %s): %s\n' \
160✔
857
                                        "$(Color Y "%q" "$output_type")" \
160✔
858
                                        "$(Color Y "%q" "$system_type")" \
160✔
859
                                        "$(Color C "%q" "$file")"
160✔
860
                                changed_files+=("$file")
40✔
861
                                continue
40✔
862
                        fi
863

864
                        if [[ "$output_type" == "directory" || "$system_type" == "directory" ]]
112✔
865
                        then
866
                                continue
16✔
867
                        fi
868

869
                        if ! diff --no-dereference --brief "$output_dir"/files/"$file" "$system_dir"/files/"$file" > /dev/null
48✔
870
                        then
871
                                Log 'Changed: %s\n' "$(Color C "%q" "$file")"
36✔
872
                                changed_files+=("$file")
18✔
873
                        fi
874
                done
875

876
        typeset -ag config_only_files=()
308✔
877

878
        ( comm -23 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
154✔
879
                while read -r -d $'\0' file
217✔
880
                do
881
                        Log 'Only in config: %s\n' "$(Color C "%q" "$file")"
126✔
882
                        config_only_files+=("$file")
63✔
883
                done
884

885
        LogLeave 'Done (%s only in system, %s changed, %s only in config).\n'        \
616✔
886
                         "$(Color G "${#system_only_files[@]}")"                                                \
616✔
887
                         "$(Color G "${#changed_files[@]}")"                                                        \
616✔
888
                         "$(Color G "${#config_only_files[@]}")"
616✔
889

890
        #
891
        # Modified file properties
892
        #
893

894
        LogEnter 'Examining file properties...\n'
154✔
895

896
        LogEnter 'Loading data...\n'
154✔
897
        unset orig_file_props # Also populated by AconfCompileSystem, so that it can be used by AconfDefaultFileProp
154✔
898
        typeset -Ag output_file_props ; AconfReadFileProps "$output_dir"/file-props.txt output_file_props
308✔
899
        typeset -Ag system_file_props ; AconfReadFileProps "$system_dir"/file-props.txt system_file_props
308✔
900
        typeset -Ag   orig_file_props ; AconfReadFileProps "$system_dir"/orig-file-props.txt orig_file_props
308✔
901
        LogLeave
154✔
902

903
        typeset -ag all_file_property_kinds
154✔
904
        all_file_property_kinds=("${!file_property_kind_exists[@]}")
154✔
905
        Print0Array all_file_property_kinds | sort --zero-terminated | mapfile -t -d $'\0' all_file_property_kinds
462✔
906

907
        AconfCompareFileProps
154✔
908

909
        LogLeave 'Done (%s only in system, %s changed, %s only in config).\n'        \
614✔
910
                         "$(Color G "${#system_only_file_props[@]}")"                                        \
614✔
911
                         "$(Color G "${#changed_file_props[@]}")"                                                \
614✔
912
                         "$(Color G "${#config_only_file_props[@]}")"
614✔
913
}
914

915
# The *_packages arrays are passed by name,
916
# so ShellCheck thinks the variables are unused:
917
# shellcheck disable=2034
918

919
# Prepare configuration and system state
920
function AconfCompile() {
921
        LogEnter 'Collecting data...\n'
152✔
922

923
        # Configuration
924

925
        AconfCompileOutput
152✔
926

927
        # System
928

929
        AconfCompileSystem
152✔
930

931
        # Vars
932

933
        < "$output_dir"/packages.txt         sort --unique | mapfile -t                   packages
304✔
934
        < "$system_dir"/packages.txt         sort --unique | mapfile -t         installed_packages
304✔
935

936
        < "$output_dir"/foreign-packages.txt sort --unique | mapfile -t           foreign_packages
304✔
937
        < "$system_dir"/foreign-packages.txt sort --unique | mapfile -t installed_foreign_packages
304✔
938

939
        AconfAnalyzeFiles
152✔
940

941
        LogLeave # Collecting data
152✔
942
}
943

944
####################################################################################################
945

946
pacman_opts=("$PACMAN")
1,733✔
947
aurman_opts=(aurman)
1,733✔
948
pacaur_opts=(pacaur)
1,733✔
949
yaourt_opts=(yaourt)
1,733✔
950
yay_opts=(yay)
1,733✔
951
paru_opts=(paru)
1,733✔
952
makepkg_opts=(makepkg)
1,733✔
953
diff_opts=(diff '--color=auto')
1,733✔
954

955
aur_helper=
1,733✔
956
aur_helpers=(aurman pacaur yaourt yay paru makepkg)
1,733✔
957

958
# Only aconfmgr can use makepkg under root
959
if [[ $EUID == 0 ]]
1,733✔
960
then
961
        aur_helper=makepkg
×
962
fi
963

964
function DetectAurHelper() {
965
        if [[ -n "$aur_helper" ]]
38✔
966
        then
967
                return
30✔
968
        fi
969

970
        LogEnter 'Detecting AUR helper...\n'
8✔
971

972
        local helper
8✔
973
        for helper in "${aur_helpers[@]}"
48✔
974
        do
975
                if hash "$helper" 2> /dev/null
48✔
976
                then
977
                        aur_helper=$helper
8✔
978
                        LogLeave '%s... Yes\n' "$(Color C %s "$helper")"
16✔
979
                        return
8✔
980
                fi
981
                Log '%s... No\n' "$(Color C %s "$helper")"
80✔
982
        done
983

984
        Log 'Can'\''t find even makepkg!?\n'
×
985
        Exit 1
×
986
}
987

988
base_devel_installed=n
1,733✔
989

990
function AconfMakePkg() {
991
        local install=true
23✔
992
        if [[ "$1" == --noinstall ]]
23✔
993
        then
994
                install=false
3✔
995
                shift
3✔
996
        fi
997

998
        local package="$1"
23✔
999
        local asdeps="${2:-false}"
23✔
1000

1001
        LogEnter 'Building foreign package %s from source.\n' "$(Color M %q "$package")"
46✔
1002

1003
        # shellcheck disable=SC2174
1004
        mkdir --parents --mode=700 "$aur_dir"
23✔
1005
        if [[ $EUID == 0 ]]
23✔
1006
        then
1007
                chown -R "$makepkg_user": "$aur_dir"
×
1008
        fi
1009

1010
        local pkg_dir="$aur_dir"/"$package"
23✔
1011
        Log 'Using directory %s.\n' "$(Color C %q "$pkg_dir")"
46✔
1012

1013
        rm -rf "$pkg_dir"
23✔
1014
        mkdir --parents "$pkg_dir"
23✔
1015

1016
        # Needed to clone the AUR repo. Should be replaced with curl/tar.
1017
        AconfNeedProgram git git n
23✔
1018

1019
        if [[ $base_devel_installed == n ]]
23✔
1020
        then
1021
                LogEnter 'Making sure the %s package is installed...\n' "$(Color M base-devel)"
28✔
1022
                ParanoidConfirm ''
14✔
1023
                if ! "$PACMAN" --query --quiet base-devel > /dev/null 2>&1
14✔
1024
                then
1025
                        AconfInstallNative base-devel
1✔
1026
                fi
1027

1028
                LogLeave
14✔
1029
                base_devel_installed=y
14✔
1030
        fi
1031

1032
        LogEnter 'Cloning...\n'
23✔
1033
        git clone "https://aur.archlinux.org/$package.git" "$pkg_dir"
23✔
1034
        LogLeave
23✔
1035

1036
        if [[ ! -f "$pkg_dir"/PKGBUILD ]]
23✔
1037
        then
1038
                Log 'No package description file found!\n'
1✔
1039

1040
                if [[ "$package" == auracle-git ]]
1✔
1041
                then
1042
                        FatalError 'Failed to download aconfmgr dependency!\n'
×
1043
                fi
1044

1045
                LogEnter 'Assuming this package is part of a package base:\n'
1✔
1046

1047
                LogEnter 'Retrieving package info...\n'
1✔
1048
                AconfNeedProgram auracle auracle-git y
1✔
1049
                local pkg_base
1✔
1050
                pkg_base=$(auracle info --format '{pkgbase}' "$package")
2✔
1051
                LogLeave 'Done, package base is %s.\n' "$(Color M %q "$pkg_base")"
2✔
1052

1053
                AconfMakePkg "$pkg_base" "$asdeps" # recurse
1✔
1054
                LogLeave # Package base
1✔
1055
                LogLeave # Package
1✔
1056
                return
1✔
1057
        fi
1058

1059
        AconfMakePkgDir "$package" "$asdeps" "$install" "$pkg_dir"
22✔
1060
}
1061

1062
function AconfMakePkgDir() {
1063
        local package=$1
22✔
1064
        local asdeps=$2
22✔
1065
        local install=$3
22✔
1066
        local pkg_dir=$4
22✔
1067

1068
        local gnupg_home
22✔
1069
        gnupg_home="$(realpath -m "$tmp_dir/gnupg")"
44✔
1070

1071
        local infofile infofilename
22✔
1072
        for infofilename in .SRCINFO .AURINFO
44✔
1073
        do
1074
                infofile="$pkg_dir"/"$infofilename"
44✔
1075
                if test -f "$infofile"
44✔
1076
                then
1077
                        LogEnter 'Checking dependencies...\n'
22✔
1078

1079
                        local depends missing_depends dependency arch
22✔
1080
                        arch="$(uname -m)"
44✔
1081
                        # Filter out packages from the same base
1082
                        ( grep -E $'^\t(make|check)?depends(_'"$arch"')? = ' "$infofile" || true ) \
32✔
1083
                                | sed 's/^.* = \([^<>=]*\)\([<>=].*\)\?$/\1/g' \
22✔
1084
                                | ( grep -vFf <(( grep '^pkgname = ' "$infofile" || true) \
66✔
1085
                                                                        | sed 's/^.* = \(.*\)$/\1/g' ) \
×
1086
                                                || true ) \
10✔
1087
                                | mapfile -t depends
22✔
1088

1089
                        if [[ ${#depends[@]} != 0 ]]
22✔
1090
                        then
1091
                                ( "$PACMAN" --deptest "${depends[@]}" || true ) | mapfile -t missing_depends
36✔
1092
                                if [[ ${#missing_depends[@]} != 0 ]]
12✔
1093
                                then
1094
                                        for dependency in "${missing_depends[@]}"
28✔
1095
                                        do
1096
                                                LogEnter '%s:\n' "$(Color M %q "$dependency")"
56✔
1097
                                                if "$PACMAN" --query --info "$dependency" > /dev/null 2>&1
28✔
1098
                                                then
1099
                                                        Log 'Already installed.\n' # Shouldn't happen, actually
×
1100
                                                elif "$PACMAN" --sync --info "$dependency" > /dev/null 2>&1
28✔
1101
                                                then
1102
                                                        Log 'Installing from repositories...\n'
22✔
1103
                                                        AconfInstallNative --asdeps "$dependency"
22✔
1104
                                                        Log 'Installed.\n'
22✔
1105
                                                else
1106
                                                        local installed=false
6✔
1107

1108
                                                        # Check if this package is provided by something in pacman repos.
1109
                                                        # `pacman -Si` will not give us that information,
1110
                                                        # however, `pacman -S` still works.
1111
                                                        AconfNeedProgram pacsift pacutils n
6✔
1112
                                                        AconfNeedProgram unbuffer expect n
6✔
1113
                                                        local providers
6✔
1114
                                                        providers=$(unbuffer pacsift --sync --exact --satisfies="$dependency")
12✔
1115
                                                        if [[ -n "$providers" ]]
6✔
1116
                                                        then
1117
                                                                Log 'Installing provider package from repositories...\n'
2✔
1118
                                                                AconfInstallNative --asdeps "$dependency"
2✔
1119
                                                                Log 'Installed.\n'
2✔
1120
                                                                installed=true
2✔
1121
                                                        fi
1122

1123
                                                        if ! $installed
6✔
1124
                                                        then
1125
                                                                Log 'Installing from AUR...\n'
4✔
1126
                                                                AconfMakePkg "$dependency" true
4✔
1127
                                                                Log 'Installed.\n'
4✔
1128
                                                        fi
1129
                                                fi
1130

1131
                                                LogLeave ''
28✔
1132
                                        done
1133
                                fi
1134
                        fi
1135

1136
                        LogLeave
22✔
1137

1138
                        local keys
22✔
1139
                        ( grep -E $'^\tvalidpgpkeys = ' "$infofile" || true ) | sed 's/^.* = \(.*\)$/\1/' | mapfile -t keys
87✔
1140
                        if [[ ${#keys[@]} != 0 ]]
22✔
1141
                        then
1142
                                LogEnter 'Checking PGP keys...\n'
1✔
1143

1144
                                local key
1✔
1145
                                for key in "${keys[@]}"
1✔
1146
                                do
1147
                                        export GNUPGHOME="$gnupg_home"
2✔
1148

1149
                                        if [[ ! -d "$GNUPGHOME" ]]
1✔
1150
                                        then
1151
                                                LogEnter 'Creating %s...\n' "$(Color C %s "$GNUPGHOME")"
2✔
1152
                                                mkdir --parents "$GNUPGHOME"
1✔
1153
                                                gpg --gen-key --batch <<EOF
1✔
1154
Key-Type: DSA
1✔
1155
Key-Length: 1024
1✔
1156
Name-Real: aconfmgr
1✔
1157
%no-protection
1✔
1158
EOF
1✔
1159
                                                LogLeave
1✔
1160
                                        fi
1✔
1161

1✔
1162
                                        LogEnter 'Adding key %s...\n' "$(Color Y %q "$key")"
2✔
1163
                                        #ParanoidConfirm ''
1✔
1164

1✔
1165
                                        local ok=false
1✔
1166
                                        local keyserver
1✔
1167
                                        for keyserver in keys.gnupg.net pgp.mit.edu pool.sks-keyservers.net keyserver.ubuntu.com # subkeys.pgp.net
1✔
1168
                                        do
1✔
1169
                                                LogEnter 'Trying keyserver %s...\n' "$(Color C %s "$keyserver")"
2✔
1170
                                                if gpg --keyserver "$keyserver" --recv-key "$key"
1✔
1171
                                                then
1✔
1172
                                                        ok=true
1✔
1173
                                                        LogLeave 'OK!\n'
1✔
1174
                                                        break
1✔
1175
                                                else
1✔
1176
                                                        LogLeave 'Error...\n'
1✔
1177
                                                fi
1✔
1178
                                        done
1✔
1179

1✔
1180
                                        if ! $ok
1✔
1181
                                        then
1✔
1182
                                                FatalError 'No keyservers succeeded.\n'
1✔
1183
                                        fi
1✔
1184

1✔
1185
                                        if [[ $EUID == 0 ]]
1✔
1186
                                        then
1✔
1187
                                                chmod 700 "$gnupg_home"
1✔
1188
                                                chown -R "$makepkg_user": "$gnupg_home"
1✔
1189
                                        fi
1✔
1190

1✔
1191
                                        LogLeave
1✔
1192
                                done
1✔
1193

1✔
1194
                                LogLeave
1✔
1195
                        fi
1✔
1196
                fi
1✔
1197
        done
1✔
1198

1✔
1199
        LogEnter 'Evaluating environment...\n'
22✔
1200
        local path
22✔
1201
        # shellcheck disable=SC2016
1✔
1202
        path=$(env -i sh -c 'source /etc/profile 1>&2 ; printf -- %s "$PATH"')
44✔
1203
        LogLeave
22✔
1204

1✔
1205
        LogEnter 'Building...\n'
22✔
1206
        (
1✔
1207
                cd "$pkg_dir"
22✔
1208
                mkdir --parents home
22✔
1209
                local args=(env -i "PATH=$path" "HOME=$PWD/home" "GNUPGHOME=$gnupg_home" "${makepkg_opts[@]}")
44✔
1210

1✔
1211
                if [[ $EUID == 0 ]]
22✔
1212
                then
1✔
1213
                        chown -R "$makepkg_user": .
1✔
1214
                        su -s /bin/bash "$makepkg_user" -c "GNUPGHOME=$(realpath ../../gnupg) $(printf ' %q' "${args[@]}")" 1>&2
1✔
1215

1✔
1216
                        if $install
1✔
1217
                        then
1✔
1218
                                local pkglist
1✔
1219
                                su -s /bin/bash "$makepkg_user" -c "GNUPGHOME=$(realpath ../../gnupg) $(printf ' %q' "${args[@]}" --packagelist)" | mapfile -t pkglist
1✔
1220

1✔
1221
                                if $asdeps
1✔
1222
                                then
1✔
1223
                                        "${pacman_opts[@]}" --upgrade --asdeps "${pkglist[@]}"
1✔
1224
                                else
1✔
1225
                                        "${pacman_opts[@]}" --upgrade "${pkglist[@]}"
1✔
1226
                                fi
1✔
1227
                        fi
1✔
1228
                else
1✔
1229
                        if $asdeps
22✔
1230
                        then
1✔
1231
                                args+=(--asdeps)
7✔
1232
                        fi
1✔
1233

1✔
1234
                        if $install
22✔
1235
                        then
1✔
1236
                                args+=(--install)
19✔
1237
                        fi
22✔
1238

22✔
1239
                        "${args[@]}" 1>&2
22✔
1240
                fi
1✔
1241
        )
1✔
1242
        LogLeave
22✔
1243

1✔
1244
        LogLeave
22✔
1245
}
1✔
1246

1✔
1247
function AconfInstallNative() {
1✔
1248
        local asdeps=false asdeps_arr=()
66✔
1249
        if [[ "$1" == --asdeps ]]
33✔
1250
        then
1✔
1251
                asdeps=true
25✔
1252
                asdeps_arr=(--asdeps)
25✔
1253
                shift
25✔
1254
        fi
1✔
1255

1✔
1256
        local target_packages=("$@")
66✔
1257
        if [[ $prompt_mode == never ]]
33✔
1258
        then
1✔
1259
                # Some prompts default to 'no'
1✔
1260
                ( yes || true ) | sudo "${pacman_opts[@]}" --confirm --sync "${asdeps_arr[@]}" "${target_packages[@]}"
1✔
1261
        else
1✔
1262
                sudo "${pacman_opts[@]}" --sync "${asdeps_arr[@]}" "${target_packages[@]}"
33✔
1263
        fi
1✔
1264
}
1✔
1265

1✔
1266
function AconfInstallForeign() {
1✔
1267
        local asdeps=false asdeps_arr=()
32✔
1268
        if [[ "$1" == --asdeps ]]
16✔
1269
        then
1✔
1270
                asdeps=true
3✔
1271
                asdeps_arr=(--asdeps)
3✔
1272
                shift
3✔
1273
        fi
1✔
1274

1✔
1275
        local target_packages=("$@")
32✔
1276

1✔
1277
        DetectAurHelper
16✔
1278

1✔
1279
        case "$aur_helper" in
16✔
1280
                aurman)
1✔
1281
                        RunExternal "${aurman_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
1✔
1282
                        ;;
1✔
1283
                pacaur)
1✔
1284
                        RunExternal "${pacaur_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
3✔
1285
                        ;;
1✔
1286
                yaourt)
1✔
1287
                        RunExternal "${yaourt_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1288
                        ;;
1✔
1289
                yay)
1✔
1290
                        RunExternal "${yay_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1291
                        ;;
1✔
1292
                paru)
1✔
1293
                        RunExternal "${paru_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1294
                        ;;
1✔
1295
                makepkg)
1✔
1296
                        local package
11✔
1297
                        for package in "${target_packages[@]}"
11✔
1298
                        do
1✔
1299
                                AconfMakePkg "$package" "$asdeps"
11✔
1300
                        done
1✔
1301
                        ;;
1✔
1302
                *)
1✔
1303
                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1304
                        false
1✔
1305
                        ;;
1✔
1306
        esac
1✔
1307
}
1✔
1308

1✔
1309
function AconfNeedProgram() {
1✔
1310
        local program="$1" # program that needs to be in PATH
248✔
1311
        local package="$2" # package the program is available in
249✔
1312
        local foreign="$3" # whether this is a foreign package
250✔
1313

1✔
1314
        if ! hash "$program" 2> /dev/null
250✔
1315
        then
1✔
1316
                if [[ $foreign == y ]]
4✔
1317
                then
1✔
1318
                        LogEnter 'Installing foreign dependency %s:\n' "$(Color M %q "$package")"
5✔
1319
                        ParanoidConfirm ''
3✔
1320
                        AconfInstallForeign --asdeps "$package"
3✔
1321
                else
1✔
1322
                        LogEnter 'Installing native dependency %s:\n' "$(Color M %q "$package")"
3✔
1323
                        ParanoidConfirm ''
2✔
1324
                        AconfInstallNative --asdeps "$package"
2✔
1325
                fi
1✔
1326
                LogLeave 'Installed.\n'
4✔
1327
        fi
1✔
1328
}
1✔
1329

1✔
1330
# Get the path to the package file (.pkg.tar.*) for the specified package.
1✔
1331
# Download or build the package if necessary.
1✔
1332
function AconfNeedPackageFile() {
1✔
1333
        set -e
36✔
1334
        local package="$1"
36✔
1335

1✔
1336
        local info foreign
36✔
1337
        if info="$(LC_ALL=C "$PACMAN" --query --info "$package")"
108✔
1338
        then
1✔
1339
                if "$PACMAN" --query --quiet --foreign "$package" > /dev/null
21✔
1340
                then
1✔
1341
                        foreign=true
6✔
1342
                else
1✔
1343
                        foreign=false
16✔
1344
                fi
1✔
1345
        else
1✔
1346
                if info="$(LC_ALL=C "$PACMAN" --sync --info "$package")"
45✔
1347
                then
1✔
1348
                        foreign=false
6✔
1349
                else
1✔
1350
                        foreign=true
10✔
1351
                fi
1✔
1352
        fi
1✔
1353

1✔
1354
        local version='' architecture='' filemask_precise filemask_any
36✔
1355
        if [[ -n "$info" ]]
36✔
1356
        then
1✔
1357
                version="$(grep '^Version' <<< "$info" | sed 's/^.* : //g')"
78✔
1358
                architecture="$(grep '^Architecture' <<< "$info" | sed 's/^.* : //g')"
78✔
1359
                filemask_precise=$(printf "%q-%q-%q.pkg.*" "$package" "$version" "$architecture")
52✔
1360
        fi
1✔
1361
        filemask_any=$(printf "%q-*-*.pkg.*" "$package")
72✔
1362

1✔
1363
        # try without downloading first
1✔
1364
        local downloaded
36✔
1365
        for downloaded in false true
45✔
1366
        do
1✔
1367
                local precise
45✔
1368
                for precise in true false
64✔
1369
                do
1✔
1370
                        # if we don't have the exact version, we can only do non-precise
1✔
1371
                        if $precise && [[ -z "$version" ]]
109✔
1372
                        then
1✔
1373
                                continue
14✔
1374
                        fi
1✔
1375

1✔
1376
                        local filemask
50✔
1377
                        if $precise
50✔
1378
                        then
1✔
1379
                                filemask=$filemask_precise
31✔
1380
                        else
1✔
1381
                                filemask=$filemask_any
19✔
1382
                        fi
1✔
1383

1✔
1384
                        local dirs=()
100✔
1385
                        if $foreign
50✔
1386
                        then
1✔
1387
                                DetectAurHelper
22✔
1388
                                local -A tried_helper=()
44✔
1389

1✔
1390
                                local helper
22✔
1391
                                for helper in "$aur_helper" "${aur_helpers[@]}"
154✔
1392
                                do
1✔
1393
                                        if [[ ${tried_helper[$helper]+x} ]]
154✔
1394
                                        then
1✔
1395
                                                continue
22✔
1396
                                        fi
1✔
1397
                                        tried_helper[$helper]=y
132✔
1398

1✔
1399
                                        case "$helper" in
132✔
1400
                                                aurman)
1✔
1401
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/aurman/$package")
22✔
1402
                                                        ;;
1✔
1403
                                                pacaur)
1✔
1404
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/pacaur/$package")
22✔
1405
                                                        ;;
1✔
1406
                                                yaourt)
1✔
1407
                                                        # yaourt does not save .pkg.xz files
1✔
1408
                                                        ;;
1✔
1409
                                                yay)
1✔
1410
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/yay/$package")
22✔
1411
                                                        ;;
1✔
1412
                                                paru)
1✔
1413
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/paru/clone/$package")
22✔
1414
                                                        ;;
1✔
1415
                                                makepkg)
1✔
1416
                                                        dirs+=("$aur_dir"/"$package")
22✔
1417
                                                        ;;
1✔
1418
                                                *)
1✔
1419
                                                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1420
                                                        false
1✔
1421
                                                        ;;
1✔
1422
                                        esac
1✔
1423
                                done
1✔
1424
                        else
1✔
1425
                                local dir
29✔
1426
                                ( LC_ALL=C pacman --verbose 2>/dev/null || true ) \
85✔
1427
                                        | sed -n 's/^Cache Dirs: \(.*\)$/\1/p' \
29✔
1428
                                        | sed 's/  /\n/g' \
29✔
1429
                                        | while read -r dir
85✔
1430
                                do
1✔
1431
                                        if [[ -n "$dir" ]]
57✔
1432
                                        then
1✔
1433
                                                dirs+=("$dir")
29✔
1434
                                        fi
1✔
1435
                                done
1✔
1436
                        fi
1✔
1437

1✔
1438
                        local files=()
100✔
1439
                        local dir
50✔
1440
                        for dir in "${dirs[@]}"
138✔
1441
                        do
1✔
1442
                                if sudo test -d "$dir"
138✔
1443
                                then
1✔
1444
                                        sudo find "$dir" -type f -name "$filemask" -not -name '*.sig' -print0 | \
44✔
1445
                                                while read -r -d $'\0' file
80✔
1446
                                                do
1✔
1447
                                                        files+=("$file")
36✔
1448
                                                done
1✔
1449
                                fi
1✔
1450
                        done
1✔
1451

1✔
1452
                        local file
50✔
1453
                        for file in "${files[@]}"
36✔
1454
                        do
1✔
1455
                                local correct
36✔
1456
                                if $precise
36✔
1457
                                then
1✔
1458
                                        correct=true
26✔
1459
                                else
1✔
1460
                                        local pkgname
10✔
1461
                                        pkgname=$(bsdtar -x --to-stdout --file "$file" .PKGINFO | \
1✔
1462
                                                                  sed -n 's/^pkgname = \(.*\)$/\1/p')
30✔
1463
                                        if [[ "$pkgname" == "$package" ]]
10✔
1464
                                        then
1✔
1465
                                                correct=true
10✔
1466
                                        else
1✔
1467
                                                correct=false
1✔
1468
                                        fi
1✔
1469
                                fi
1✔
1470

1✔
1471
                                if $correct
36✔
1472
                                then
1✔
1473
                                        printf '%s' "$file"
36✔
1474
                                        return
36✔
1475
                                fi
1✔
1476
                        done
1✔
1477
                done
1✔
1478

1✔
1479
                if $downloaded
10✔
1480
                then
1✔
1481
                        Log 'Unable to find package file for package %s!\n' "$(Color M %q "$package")"
1✔
1482
                        Exit 1
1✔
1483
                else
1✔
1484
                        if $foreign
10✔
1485
                        then
1✔
1486
                                LogEnter 'Building foreign package %s\n' "$(Color M %q "$package")"
11✔
1487
                                ParanoidConfirm ''
6✔
1488

1✔
1489
                                local helper
6✔
1490
                                for helper in "$aur_helper" "${aur_helpers[@]}"
12✔
1491
                                do
1✔
1492
                                        case "$helper" in
12✔
1493
                                                aurman)
1✔
1494
                                                        # aurman does not have a --makepkg option
1✔
1495
                                                        ;;
1✔
1496
                                                pacaur)
1✔
1497
                                                        if command -v "${pacaur_opts[0]}" > /dev/null
4✔
1498
                                                        then
1✔
1499
                                                                RunExternal "${pacaur_opts[@]}" --makepkg --aur --makepkg "$package" 1>&2
3✔
1500
                                                                break
3✔
1501
                                                        fi
1✔
1502
                                                        ;;
1✔
1503
                                                yaourt)
1✔
1504
                                                        # yaourt does not save .pkg.xz files
1✔
1505
                                                        continue
3✔
1506
                                                        ;;
1✔
1507
                                                yay)
1✔
1508
                                                        # yay does not have a --makepkg option
1✔
1509
                                                        continue
2✔
1510
                                                        ;;
1✔
1511
                                                paru)
1✔
1512
                                                        # paru does not have a --makepkg option
1✔
1513
                                                        continue
2✔
1514
                                                        ;;
1✔
1515
                                                makepkg)
1✔
1516
                                                        AconfMakePkg --noinstall "$package"
4✔
1517
                                                        break
4✔
1518
                                                        ;;
1✔
1519
                                                *)
1✔
1520
                                                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1521
                                                        false
1✔
1522
                                                        ;;
1✔
1523
                                        esac
1✔
1524
                                done
1✔
1525

1✔
1526
                                LogLeave
6✔
1527
                        else
1✔
1528
                                LogEnter "Downloading package %s (%s) to pacman's cache\\n" "$(Color M %q "$package")" "$(Color C %s "$filemask_precise")"
13✔
1529
                                ParanoidConfirm ''
5✔
1530
                                sudo "$PACMAN" --sync --download --nodeps --nodeps --noconfirm "$package" 1>&2
5✔
1531
                                LogLeave
5✔
1532
                        fi
1✔
1533
                fi
1✔
1534
        done
1✔
1535
}
1✔
1536

1✔
1537
# Extract the original file from a package to stdout
1✔
1538
function AconfGetPackageOriginalFile() {
1✔
1539
        local package="$1" # Package to extract the file from
31✔
1540
        local file="$2" # Absolute path to file in package
31✔
1541

1✔
1542
        local package_file
31✔
1543
        package_file="$(AconfNeedPackageFile "$package")"
62✔
1544

1✔
1545
        local args=(bsdtar -x --to-stdout --file "$package_file" "${file/\//}")
62✔
1546
        if [[ -r "$package_file" ]]
31✔
1547
        then
22✔
1548
                "${args[@]}"
23✔
1549
        else
1✔
1550
                sudo "${args[@]}"
9✔
1551
        fi
1✔
1552
}
1✔
1553

1✔
1554
function AconfRestoreFile() {
1✔
1555
        local package=$1
6✔
1556
        local file=$2
6✔
1557

1✔
1558
        local package_file
6✔
1559
        package_file="$(AconfNeedPackageFile "$package")"
11✔
1560

1✔
1561
        # If we are restoring a directory, it may be non-empty.
1✔
1562
        # Extract the object to a temporary location first.
1✔
1563
        local tmp_base=${tmp_dir:?}/dir-props
6✔
1564
        sudo rm -rf "$tmp_base"
6✔
1565

1✔
1566
        mkdir -p "$tmp_base"
6✔
1567
        local tmp_file="$tmp_base""$file"
6✔
1568
        sudo tar x --directory "$tmp_base" --file "$package_file" --no-recursion "${file/\//}"
6✔
1569

1✔
1570
        AconfReplace "$tmp_file" "$file"
6✔
1571
        sudo rm -rf "$tmp_base"
6✔
1572
}
1✔
1573

1✔
1574
# Move filesystem object at $1 to $2, replacing any existing one.
1✔
1575
# Attempt to do so atomically, when possible.
1✔
1576
# Do the right thing when filesystem objects differ, but never
1✔
1577
# recursively remove directories (copy their attributes instead).
1✔
1578
function AconfReplace() {
1✔
1579
        local src=$1
6✔
1580
        local dst=$2
6✔
1581

1✔
1582
        # Try direct mv first
1✔
1583
        if ! sudo mv --no-target-directory "$src" "$dst" 2>/dev/null
6✔
1584
        then
1✔
1585
                # Direct mv failed - directory or object type mismatch
1✔
1586
                if sudo rm --force --dir "$dst" 2>/dev/null
3✔
1587
                then
1✔
1588
                        # Deleted target successfully, now overwrite it
1✔
1589
                        sudo mv --no-target-directory "$src" "$dst"
3✔
1590
                else
1✔
1591
                        # rm failed - likely a non-empty directory; copy
1✔
1592
                        # attributes only
1✔
1593
                        sudo chmod --reference="$src" "$dst"
1✔
1594
                        sudo chown --reference="$src" "$dst"
1✔
1595
                        sudo touch --reference="$src" "$dst"
1✔
1596
                fi
1✔
1597
        fi
1✔
1598
}
1✔
1599

1✔
1600
####################################################################################################
1✔
1601

1✔
1602
prompt_mode=normal # never / normal / paranoid
1,733✔
1603

1✔
1604
function Confirm() {
1✔
1605
        local detail_func="$1"
1✔
1606

1✔
1607
        if [[ $prompt_mode == never ]]
1✔
1608
        then
1✔
1609
                return
1✔
1610
        fi
1✔
1611

1✔
1612
        while true
1✔
1613
        do
1✔
1614
                if [[ -n "$detail_func" ]]
1✔
1615
                then
1✔
1616
                        Log 'Proceed? [Y/n/d] '
1✔
1617
                else
1✔
1618
                        Log 'Proceed? [Y/n] '
1✔
1619
                fi
1✔
1620
                read -r -n 1 answer < /dev/tty
1✔
1621
                echo 1>&2
1✔
1622
                case "$answer" in
1✔
1623
                        Y|y|'')
1✔
1624
                                return
1✔
1625
                                ;;
1✔
1626
                        N|n)
1✔
1627
                                Log '%s\n' "$(Color R "User abort")"
1✔
1628
                                Exit 1
1✔
1629
                                ;;
1✔
1630
                        D|d)
1✔
1631
                                $detail_func
1✔
1632
                                continue
1✔
1633
                                ;;
1✔
1634
                        *)
1✔
1635
                                continue
1✔
1636
                                ;;
1✔
1637
                esac
1✔
1638
        done
1✔
1639
}
1✔
1640

1✔
1641
function ParanoidConfirm() {
1✔
1642
        if [[ $prompt_mode == paranoid ]]
156✔
1643
        then
1✔
1644
                Confirm "$@"
156✔
1645
        fi
1✔
1646
}
1✔
1647

1✔
1648
####################################################################################################
1✔
1649

1✔
1650
log_indent=:
1,733✔
1651

1✔
1652
function Log() {
1✔
1653
        if [[ "$#" != 0 && -n "$1" ]]
159,584✔
1654
        then
1✔
1655
                local fmt="$1"
79,212✔
1656
                shift
79,215✔
1657

1✔
1658
                if [[ -z $ANSI_clear_line ]]
79,200✔
1659
                then
1✔
1660
                        # Replace carriage returns in format string with newline
1✔
1661
                        # when colors are disabled. This avoids systemd's journal
1✔
1662
                        # from showing such lines as [# blob data].
1✔
1663

1✔
1664
                        fmt=${fmt//\\r/\\n} # Replace the '\r' sequence
1✔
1665
                                            # (backslash-r) , not actual carriage
1✔
1666
                                            # returns.
1✔
1667
                fi
1✔
1668

1✔
1669
                printf "${ANSI_clear_line}${ANSI_color_B}%s ${ANSI_color_W}${fmt}${ANSI_reset}" "$log_indent" "$@" 1>&2
79,199✔
1670
        fi
1✔
1671
}
1✔
1672

1✔
1673
function LogEnter() {
1✔
1674
        Log "$@"
5,634✔
1675
        log_indent=$log_indent:
5,623✔
1676
}
1✔
1677

1✔
1678
function LogLeave() {
1✔
1679
        if [[ $# == 0 ]]
5,623✔
1680
        then
1✔
1681
                Log 'Done.\n'
3,494✔
1682
        else
1✔
1683
                Log "$@"
2,131✔
1684
        fi
1✔
1685

1✔
1686
        log_indent=${log_indent::-1}
5,627✔
1687
}
1✔
1688

1✔
1689
function ConfigWarning() {
1✔
1690
        Log '%s: '"$1" "$(Color Y "Warning")" "${@:2}"
17✔
1691
        printf W >> "$output_dir"/warnings
9✔
1692
}
1✔
1693

1✔
1694
function FatalError() {
1✔
1695
        Log "$@"
49✔
1696
        false
97✔
1697
        # if we're here, errexit is not set
1✔
1698
        Log 'Continuing after error. This is a bug, please report it.\n'
1✔
1699
        Exit 1
1✔
1700
}
1✔
1701

1✔
1702
function Color() {
1✔
1703
        local var="ANSI_color_$1"
71,990✔
1704
        printf -- "%s" "${!var}"
71,992✔
1705
        shift
72,004✔
1706
        # shellcheck disable=2059
1✔
1707
        printf -- "$@"
71,994✔
1708
        printf -- "%s" "${ANSI_color_W}"
71,995✔
1709
}
1✔
1710

1✔
1711
# The ANSI_color_* variables are looked up by name:
1✔
1712
# shellcheck disable=2034
1✔
1713
function DisableColor() {
1✔
1714
        ANSI_color_R=
1✔
1715
        ANSI_color_G=
1✔
1716
        ANSI_color_Y=
1✔
1717
        ANSI_color_B=
1✔
1718
        ANSI_color_M=
1✔
1719
        ANSI_color_C=
1✔
1720
        ANSI_color_W=
1✔
1721
        ANSI_reset=
1✔
1722
        ANSI_clear_line=
1✔
1723
}
1✔
1724

1✔
1725
####################################################################################################
1✔
1726

1✔
1727
function OnError() {
1✔
1728
        trap '' EXIT ERR
51✔
1729

1✔
1730
        LogEnter '%s! Stack trace:\n' "$(Color R "Fatal error")"
101✔
1731

1✔
1732
        local frame=0 str
51✔
1733
        while str=$(caller $frame)
397✔
1734
        do
1✔
1735
                if [[ $str =~ ^([^\ ]*)\ ([^\ ]*)\ (.*)$ ]]
149✔
1736
                then
1✔
1737
                        Log '%s:%s [%s]\n' "$(Color C "%q" "${BASH_REMATCH[3]}")" "$(Color G "%q" "${BASH_REMATCH[1]}")" "$(Color Y "%q" "${BASH_REMATCH[2]}")"
593✔
1738
                else
1✔
1739
                        Log '%s\n' "$str"
1✔
1740
                fi
1✔
1741

1✔
1742
                frame=$((frame+1))
149✔
1743
        done
1✔
1744

1✔
1745
        LogLeave ''
51✔
1746

1✔
1747
        if [[ -d "$tmp_dir" ]]
51✔
1748
        then
1✔
1749
                local df dir
51✔
1750
                df=$(($(stat -f --format="%a*%S" "$tmp_dir")))
101✔
1751
                if ! dir="$(realpath "$(dirname "$tmp_dir")" 2> /dev/null)"
151✔
1752
                then
1✔
1753
                        dir="$(dirname "$tmp_dir")"
1✔
1754
                fi
1✔
1755
                if [[ $df -lt $warn_tmp_df_threshold ]]
51✔
1756
                then
1✔
1757
                        LogEnter 'Probable cause: low disk space (%s bytes) in %s. Suggestions:\n' "$(Color G %s "$df")" "$(Color C %q "$dir")"
1✔
1758
                        Log '- Ignore more files and directories using %s directives;\n' "$(Color Y IgnorePath)"
1✔
1759
                        Log '- Free up more space in %s;\n' "$(Color C %q "$dir")"
1✔
1760
                        Log '- Set %s to another location before invoking %s.\n' "$(Color Y \$TMPDIR)" "$(Color Y aconfmgr)"
1✔
1761
                        LogLeave ''
1✔
1762
                fi
1✔
1763
        fi
1✔
1764

1✔
1765
        # Ensure complete abort when inside a string expansion
1✔
1766
        exit 1
51✔
1767
}
1✔
1768
trap OnError EXIT ERR
1,733✔
1769

1✔
1770
function Exit() {
1✔
1771
        trap '' EXIT ERR
1,682✔
1772
        exit "${1:-0}"
1,682✔
1773
}
1✔
1774

1✔
1775
####################################################################################################
1✔
1776

1✔
1777
# Print an array, one element per line (assuming IFS starts with \n).
1✔
1778
function PrintArray() {
1✔
1779
        local name="$1" # Name of the global variable containing the array
2,062✔
1780
        local size
2,062✔
1781

1✔
1782
        size="$(eval "echo \${#${name}""[*]}")"
6,186✔
1783
        if [[ $size != 0 ]]
2,062✔
1784
        then
1✔
1785
                eval "echo \"\${${name}[*]}\""
1,552✔
1786
        fi
1✔
1787
}
1✔
1788

1✔
1789
# Ditto, but terminate elements with a NUL.
1✔
1790
function Print0Array() {
1✔
1791
        local name="$1" # Name of the global variable containing the array
1,947✔
1792

1✔
1793
        eval "$(cat <<EOF
1✔
1794
        if [[ \${#${name}[*]} != 0 ]]
1✔
1795
        then
1✔
1796
                local item
1✔
1797
                for item in "\${${name}[@]}"
1✔
1798
                do
1✔
1799
                        printf '%s\\0' "\$item"
1✔
1800
                done
1✔
1801
        fi
1✔
1802
EOF
1✔
1803
)"
5,841✔
1804
}
1805

1,328✔
1806
# Ditto, but shell-escape array elements.
10,199✔
1807
function PrintQArray() {
1808
        local name="$1" # Name of the global variable containing the array
11,058✔
1809
        local size
906✔
1810

1811
        size="$(eval "echo \${#${name}""[*]}")"
2,718✔
1812
        if [[ $size != 0 ]]
906✔
1813
        then
1814
                eval "printf -- %q \"\${${name}[0]}\""
8✔
1815
                if [[ $size -gt 1 ]]
4✔
1816
                then
1817
                        eval "printf -- ' %q' \"\${${name}[@]:1}\""
2✔
1818
                fi
1819
        fi
1820
}
1821

1822
if [[ $EUID == 0 ]]
1,733✔
1823
then
1824
        function sudo() { "$@" ; }
1825
fi
1826

1827
# Run external bash script.
1828
# No-op, except when running under the test suite with bashcov,
1829
# in which case it does not propagate bashcov to the invoked script.
1830
# Unlike `env -i`, does not nuke the environment.
1831
function RunExternal() {
1832
        env -u SHELLOPTS -u PS4 -u SHLVL -u BASH_XTRACEFD "$@"
8✔
1833
}
1834

1835
# cat a file; if it's not readable, cat via sudo.
1836
function SuperCat() {
1837
        local file="$1"
2✔
1838

1839
        if [[ -r "$1" ]]
2✔
1840
        then
1841
                cat "$1"
1✔
1842
        else
1843
                sudo cat "$1"
1✔
1844
        fi
1845
}
1846

1847
if ! ( empty_array=() ; : "${empty_array[@]}" )
3,466✔
1848
then
1849
        # Old bash versions treat substitution of an empty array
1850
        # synonymous with substituting an unset variable, signaling an
1851
        # error in -u mode and stopping the script in -e mode. We
1852
        # generally want both of these enabled to catch bugs early and
1853
        # fail fast, but making every array substitution conditional on
1854
        # whether it is empty or not is unreasonably onerous, so just
1855
        # disable those checks in old bash versions - it is then up to the
1856
        # test suite ran against newer bash versions to ensure code
1857
        # correctness.
1858

1859
        Log '%s: Old bash detected, disabling unset variable checking.\n' "$(Color Y "Warning")"
×
1860
        set +u
×
1861
fi
1862

1863
: # include in coverage
1,733✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc