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

CyberShadow / aconfmgr / 625

18 Nov 2025 11:56AM UTC coverage: 92.653% (+13.0%) from 79.69%
625

push

github

CyberShadow
.github/workflows/test.yml: Disable fail-fast

The integration test depends on flaky network services. We would like
to be able to retry just the failed jobs, which is not possible when
GitHub cancels all jobs when any fails.

3884 of 4192 relevant lines covered (92.65%)

390.53 hits per line

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

93.72
/src/apply.bash
1
# apply.bash
2

3
# This file contains the implementation of aconfmgr's 'apply' command.
4

5
function AconfApply() {
6
        local modified=n
56✔
7

8
        function PrintFileProperty() {
9
                local kind="$1"
69✔
10
                local value="$2"
69✔
11
                local file="$3"
69✔
12

13
                local value_text
69✔
14
                if [[ -z "$value" ]]
69✔
15
                then
16
                        local default_value
10✔
17
                        default_value="$(AconfDefaultFileProp "$file" "$kind")"
20✔
18
                        value_text="$(printf '%s (default value)' "$(Color G "%s" "$default_value")")"
30✔
19
                else
20
                        value_text="$(Color G "%s" "$value")"
118✔
21
                fi
22

23
                Log 'Setting %s of %s to %s\n'        \
207✔
24
                        "$(Color Y "%s" "$kind")"        \
207✔
25
                        "$(Color C "%q" "$file")"        \
207✔
26
                        "$value_text"
207✔
27
        }
28

29
        function ApplyFileProperty() {
30
                local kind="$1"
58✔
31
                local value="$2"
58✔
32
                local file="$3"
58✔
33

34
                PrintFileProperty "$kind" "$value" "$file"
58✔
35

36
                if [[ -z "$value" ]]
58✔
37
                then
38
                        value="$(AconfDefaultFileProp "$file" "$kind")"
10✔
39
                fi
40

41
                case "$kind" in
58✔
42
                        mode)
43
                                sudo chmod "$value" "$file"
18✔
44
                                ;;
45
                        owner)
46
                                sudo chown --no-dereference "$value" "$file"
20✔
47
                                ;;
48
                        group)
49
                                sudo chgrp --no-dereference "$value" "$file"
20✔
50
                                ;;
51
                        *)
52
                                Log 'Unknown property %s with value %s for file %s\n' \
×
53
                                        "$(Color Y "%q" "$kind" )" \
×
54
                                        "$(Color G "%q" "$value")" \
×
55
                                        "$(Color C "%q" "$file" )"
×
56
                                Exit 1
×
57
                                ;;
58
                esac
59
        }
60

61
        function ApplyFileProps() {
62
                local file="$1"
40✔
63
                local prop
40✔
64

65
                for prop in "${all_file_property_kinds[@]}"
126✔
66
                do
67
                        local key="$file:$prop"
126✔
68
                        if [[ -n "${output_file_props[$key]+x}" ]]
126✔
69
                        then
70
                                local value="${output_file_props[$key]}"
47✔
71
                                ApplyFileProperty "$prop" "$value" "$file"
47✔
72
                                unset "output_file_props[\$key]"
47✔
73
                                unset "system_file_props[\$key]"
47✔
74

75
                                printf '%s\t%s\t%q\n' "$prop" "$value" "$file" >> "$system_dir"/file-props.txt
47✔
76
                        fi
77
                done
78
        }
79

80
        function InstallFile() {
81
                local file="$1"
40✔
82
                local source="$output_dir"/files/"$file"
40✔
83

84
                # system
85

86
                local target="$file".aconfmgr-new
40✔
87
                sudo rm --force --dir "$target"
40✔
88

89
                # Is the target a directory?
90
                if sudo test ! -h "$file" -a -d "$file"
40✔
91
                then
92
                        # Is the source a directory?
93
                        if test ! -h "$source" -a -d "$source"
7✔
94
                        then
95
                                # Update the target's properties in-place.
96
                                target=$file
5✔
97
                        else
98
                                # Delete the existing directory.
99
                                # install/mv can't replace a directory with a non-directory.
100
                                sudo rm --force --dir "$file"
2✔
101
                        fi
102
                else
103
                        # Is the source a directory?
104
                        if test ! -h "$source" -a -d "$source"
33✔
105
                        then
106
                                # Delete the existing non-directory.
107
                                # install/mv can't replace a non-directory with a directory either.
108
                                sudo rm --force "$file"
7✔
109
                        fi
110
                fi
111

112
                sudo mkdir --parents "$(dirname "$target")"
80✔
113
                if [[ -h "$source" ]]
40✔
114
                then
115
                        sudo cp --no-dereference "$source" "$target"
6✔
116
                        sudo chown --no-dereference root:root "$target"
6✔
117
                elif [[ -d "$source" ]]
34✔
118
                then
119
                        sudo install -d \
12✔
120
                                 --mode="${output_file_props[$file:mode]:-755}" \
12✔
121
                                 --owner="${output_file_props[$file:owner]:-0}" \
12✔
122
                                 --group="${output_file_props[$file:group]:-0}" \
12✔
123
                                 "$target"
12✔
124
                else
125
                        sudo install \
22✔
126
                                 --mode="${output_file_props[$file:mode]:-$default_file_mode}" \
22✔
127
                                 --owner="${output_file_props[$file:owner]:-0}" \
22✔
128
                                 --group="${output_file_props[$file:group]:-0}" \
22✔
129
                                 "$source" "$target"
22✔
130
                fi
131

132
                if [[ "$target" != "$file" ]]
40✔
133
                then
134
                        sudo mv --force --no-target-directory "$target" "$file"
35✔
135
                fi
136

137
                # $system_dir
138

139
                local system_file="$system_dir"/files/"$file"
40✔
140
                if ! { test ! -h "$source" -a -d "$source" && test ! -h "$system_file" -a -d "$system_file" ; }
52✔
141
                then
142
                        rm --force --dir "$system_file"
38✔
143
                fi
144

145
                mkdir --parents "$(dirname "$system_file")"
80✔
146
                if [[ -h "$source" ]]
40✔
147
                then
148
                        cp --no-dereference "$source" "$system_file"
6✔
149
                elif [[ -d "$source" ]]
34✔
150
                then
151
                        mkdir --parents "$system_file"
12✔
152
                else
153
                        cp --no-dereference "$source" "$system_file"
22✔
154
                fi
155

156
                ApplyFileProps "$file"
40✔
157

158
                if [[ -h "$source" ]]
40✔
159
                then
160
                        # ApplyFileProps will apply and unset owner/group. For
161
                        # symlinks, we need to avoid attempting to restore the
162
                        # mode, so unset it here.
163
                        unset "system_file_props[\$file:mode]"
6✔
164
                        printf '%s\t%s\t%q\n' mode '' "$file" >> "$system_dir"/file-props.txt
6✔
165
                fi
166
        }
167

168
        AconfCompile
56✔
169

170
        LogEnter 'Applying configuration...\n'
55✔
171

172
        #
173
        # Priority files
174
        #
175

176
        LogEnter 'Installing priority files...\n'
56✔
177

178
        # shellcheck disable=SC2329  # Callback function invoked indirectly
179
        function Details_DiffFile() {
180
                if sudo test -d "$file"
15✔
181
                then
182
                        Log '%s (old) is a directory.\n' "$(Color C "%q" "$file")"
8✔
183
                elif test -d "$output_dir"/files/"$file"
12✔
184
                then
185
                        Log '%s (new) is a directory.\n' "$(Color C "%q" "$file")"
6✔
186
                else
187
                        AconfNeedProgram diff diffutils n
9✔
188
                        sudo "${diff_opts[@]}" --unified --no-dereference --report-identical-files "$file" "$output_dir"/files/"$file" || true
18✔
189
                fi
190
        }
191

192
        local file
56✔
193
        comm -12 --zero-terminated \
336✔
194
                 <(  Print0Array priority_files                                 | sort --zero-terminated ) \
×
195
                 <( (Print0Array config_only_files ; Print0Array changed_files) | sort --zero-terminated ) | \
×
196
                while read -r -d $'\0' file
63✔
197
                do
198
                        LogEnter 'Installing %s...\n' "$(Color C %q "$file")"
13✔
199

200
                        if sudo test -e "$file"
6✔
201
                        then
202
                                Confirm Details_DiffFile
4✔
203
                        else
204
                                Confirm ''
3✔
205
                        fi
206

207
                        InstallFile "$file"
7✔
208
                        LogLeave
7✔
209
                        modified=y
7✔
210
                done
211
        comm -23 --zero-terminated <(Print0Array config_only_files | sort --zero-terminated) <(Print0Array priority_files | sort --zero-terminated) | mapfile -d $'\0' config_only_files
336✔
212
        comm -23 --zero-terminated <(Print0Array changed_files     | sort --zero-terminated) <(Print0Array priority_files | sort --zero-terminated) | mapfile -d $'\0' changed_files
336✔
213

214
        LogLeave # Installing priority files
56✔
215

216
        if (LC_ALL=C "$PACMAN" 2>&1 || true) | grep --extended-regexp --only-matching 'database file for .+ does not exist'
224✔
217
        then
218
                Log 'The pacman local package database is internally inconsistent. Perform a full refresh and system upgrade now?\n'
1✔
219
                Confirm ''
1✔
220

221
                LogEnter 'Performing a full refresh and system upgrade...\n'
1✔
222
                sudo "$PACMAN" --sync --refresh --sysupgrade --noconfirm
1✔
223
                LogLeave
1✔
224
        fi
225

226
        #
227
        # Apply packages
228
        #
229

230
        LogEnter 'Configuring packages...\n'
56✔
231

232
        #        in                in                in-
233
        #        config        system        stalled foreign        action
234
        #        ----------------------------------------
235
        #        no                no                no                no                nothing
236
        #        no                no                no                yes                nothing
237
        #        no                no                yes                no                (prune)
238
        #        no                no                yes                yes                (prune)
239
        #        no                yes                no                no                impossible
240
        #        no                yes                no                yes                impossible
241
        #        no                yes                yes                no                unpin (and prune)
242
        #        no                yes                yes                yes                unpin (and prune)
243
        #        yes                no                no                no                install via pacman
244
        #        yes                no                no                yes                install via makepkg
245
        #        yes                no                yes                no                pin
246
        #        yes                no                yes                yes                pin
247
        #        yes                yes                no                no                impossible
248
        #        yes                yes                no                yes                impossible
249
        #        yes                yes                yes                no                nothing
250
        #        yes                yes                yes                yes                nothing
251

252
        # Unknown packages (native and foreign packages that are explicitly installed but not listed)
253
        local -a unknown_packages
56✔
254
        comm -13                                                                               \
392✔
255
                 <((PrintArray           packages ; PrintArray           foreign_packages) | sort) \
×
256
                 <((PrintArray installed_packages ; PrintArray installed_foreign_packages) | sort) \
×
257
                 | mapfile -t unknown_packages
56✔
258

259
        if [[ ${#unknown_packages[@]} != 0 ]]
56✔
260
        then
261
                LogEnter 'Unpinning %s unknown packages.\n' "$(Color G ${#unknown_packages[@]})"
12✔
262

263
                # shellcheck disable=SC2329  # Callback function invoked indirectly
264
                function Details() { Log 'Unpinning (setting install reason to '\''as dependency'\'') the following packages:%s\n' "$(Color M " %q" "${unknown_packages[@]}")" ; }
12✔
265
                Confirm Details
6✔
266

267
                Print0Array unknown_packages | sudo xargs -0 "$PACMAN" --database --asdeps
12✔
268

269
                modified=y
6✔
270
                LogLeave
6✔
271
        fi
272

273
        # Missing packages (native and foreign packages that are listed in the configuration, but not marked as explicitly installed)
274
        local -a missing_packages
56✔
275
        comm -23                                                                                                                                                           \
392✔
276
                 <((PrintArray           packages ; PrintArray           foreign_packages) | sort) \
×
277
                 <((PrintArray installed_packages ; PrintArray installed_foreign_packages) | sort) \
×
278
                 | mapfile -t missing_packages
56✔
279

280
        # Missing installed/unpinned packages (native and foreign packages that are implicitly installed,
281
        # and listed in the configuration, but not marked as explicitly installed)
282
        local -a missing_unpinned_packages
56✔
283
        comm -12 <(PrintArray missing_packages) <(("$PACMAN" --query --quiet || true) | sort) | mapfile -t missing_unpinned_packages
280✔
284

285
        if [[ ${#missing_unpinned_packages[@]} != 0 ]]
56✔
286
        then
287
                LogEnter 'Pinning %s unknown packages.\n' "$(Color G ${#missing_unpinned_packages[@]})"
6✔
288

289
                # shellcheck disable=SC2329  # Callback function invoked indirectly
290
                function Details() { Log 'Pinning (setting install reason to '\''explicitly installed'\'') the following packages:%s\n' "$(Color M " %q" "${missing_unpinned_packages[@]}")" ; }
6✔
291
                Confirm Details
3✔
292

293
                Print0Array missing_unpinned_packages | sudo xargs -0 "$PACMAN" --database --asexplicit
6✔
294

295
                modified=y
3✔
296
                LogLeave
3✔
297
        fi
298

299
        # Orphan packages
300

301
        local -a files_in_deleted_packages=()
112✔
302

303
        if ( "$PACMAN" --query --unrequired --unrequired --deps --quiet --native || true ) |
102✔
304
                   grep -qvFxf <(PrintArray ignore_packages        ) > /dev/null ||
112✔
305
           ( "$PACMAN" --query --unrequired --unrequired --deps --quiet --foreign || true ) |
91✔
306
                   grep -qvFxf <(PrintArray ignore_foreign_packages) > /dev/null
92✔
307
        then
308
                LogEnter 'Pruning orphan packages...\n'
11✔
309

310
                # We have to loop, since pacman's dependency scanning doesn't seem to be recursive
311
                local iter=1
11✔
312
                while true
29✔
313
                do
314
                        LogEnter 'Iteration %s:\n' "$(Color G "$iter")"
58✔
315

316
                        LogEnter 'Querying orphan packages...\n'
29✔
317
                        local -a orphan_packages
29✔
318
                        {
319
                                ( "$PACMAN" --query --unrequired --unrequired --deps --quiet --native || true ) |
39✔
320
                                        ( grep -vFxf <(PrintArray ignore_packages        ) || true )
70✔
321
                                ( "$PACMAN" --query --unrequired --unrequired --deps --quiet --foreign || true ) |
49✔
322
                                        ( grep -vFxf <(PrintArray ignore_foreign_packages) || true )
80✔
323
                        } | mapfile -t orphan_packages
29✔
324
                        LogLeave
29✔
325

326
                        if [[ ${#orphan_packages[@]} != 0 ]]
29✔
327
                        then
328
                                LogEnter 'Pruning %s orphan packages.\n' "$(Color G ${#orphan_packages[@]})"
36✔
329

330
                                # shellcheck disable=SC2329  # Callback function invoked indirectly
331
                                function Details() { Log 'Removing the following orphan packages:%s\n' "$(Color M " %q" "${orphan_packages[@]}")" ; }
36✔
332
                                ParanoidConfirm Details
18✔
333

334
                                local -a deleted_files=()
36✔
335
                                "$PACMAN" --query --list --quiet "${orphan_packages[@]}" | sed 's#^\(.*\)/$#\1#' | mapfile -t deleted_files
54✔
336
                                files_in_deleted_packages+=("${deleted_files[@]}")
18✔
337

338
                                sudo "${pacman_opts[@]}" --remove "${orphan_packages[@]}"
18✔
339

340
                                LogLeave
18✔
341
                        fi
342

343
                        iter=$((iter+1))
29✔
344

345
                        LogLeave # Iteration
29✔
346

347
                        if [[ ${#orphan_packages[@]} == 0 ]]
29✔
348
                        then
349
                                break
11✔
350
                        fi
351
                done
352

353
                modified=y
11✔
354
                LogLeave # Removing orphan packages
11✔
355
        fi
356

357

358
        # Missing native packages (native packages that are listed in the configuration, but not installed)
359
        local -a missing_native_packages
56✔
360
        comm -23 <(PrintArray packages) <(("$PACMAN" --query --quiet || true) | sort) | mapfile -t missing_native_packages
280✔
361

362
        if [[ ${#missing_native_packages[@]} != 0 ]]
56✔
363
        then
364
                LogEnter 'Installing %s missing native packages.\n' "$(Color G ${#missing_native_packages[@]})"
8✔
365

366
                # shellcheck disable=SC2329  # Callback function invoked indirectly
367
                function Details() { Log 'Installing the following native packages:%s\n' "$(Color M " %q" "${missing_native_packages[@]}")" ; }
8✔
368
                ParanoidConfirm Details
4✔
369

370
                AconfInstallNative "${missing_native_packages[@]}"
4✔
371

372
                modified=y
4✔
373
                LogLeave
4✔
374
        fi
375

376
        # Missing foreign packages (foreign packages that are listed in the configuration, but not installed)
377
        local -a missing_foreign_packages
56✔
378
        comm -23 <(PrintArray foreign_packages) <(("$PACMAN" --query --quiet || true) | sort) | mapfile -t missing_foreign_packages
280✔
379

380
        if [[ ${#missing_foreign_packages[@]} != 0 ]]
56✔
381
        then
382
                LogEnter 'Installing %s missing foreign packages.\n' "$(Color G ${#missing_foreign_packages[@]})"
28✔
383

384
                # shellcheck disable=SC2329  # Callback function invoked indirectly
385
                function Details() { Log 'Installing the following foreign packages:%s\n' "$(Color M " %q" "${missing_foreign_packages[@]}")" ; }
28✔
386
                Confirm Details
14✔
387

388
                # If an AUR helper is present in the list of packages to be installed,
389
                # install it first, then use it to install the rest of the foreign packages.
390
                function InstallAurHelper() {
391
                        local package helper
14✔
392
                        for package in "${missing_foreign_packages[@]}"
15✔
393
                        do
394
                                for helper in "${aur_helpers[@]}"
100✔
395
                                do
396
                                        if [[ "$package" == "$helper" ]]
100✔
397
                                        then
398
                                                LogEnter 'Installing AUR helper %s...\n' "$(Color M %q "$helper")"
2✔
399
                                                ParanoidConfirm ''
1✔
400
                                                AconfInstallForeign "$package"
1✔
401
                                                aur_helper="$package"
1✔
402
                                                LogLeave
1✔
403
                                                return
1✔
404
                                        fi
405
                                done
406
                        done
407
                }
408
                if [[ $EUID != 0 ]]
14✔
409
                then
410
                        InstallAurHelper
14✔
411
                fi
412

413
                AconfInstallForeign "${missing_foreign_packages[@]}"
14✔
414

415
                modified=y
14✔
416
                LogLeave
14✔
417
        fi
418

419
        LogLeave # Configuring packages
56✔
420

421
        # Read file owners
422
        local -a modified_files_in_deleted_packages
56✔
423
        (
424
                cat "$tmp_dir"/output-files "$tmp_dir"/system-files
56✔
425
                local kind value file
56✔
426
                cat "$output_dir"/file-props.txt "$system_dir"/file-props.txt \
56✔
427
                        | \
×
428
                        while IFS=$'\t' read -r kind value file
476✔
429
                        do
430
                                eval "printf '%s\\0' $file" # Unescape
364✔
431
                        done
432
        ) \
×
433
                | sort --zero-terminated --unique \
56✔
434
                | comm --zero-terminated -12 <(Print0Array files_in_deleted_packages | sort --zero-terminated --unique) /dev/stdin \
168✔
435
                | tac -s $'\0' \
56✔
436
                | mapfile -t -d $'\0' modified_files_in_deleted_packages
56✔
437

438
        if [[ "${#modified_files_in_deleted_packages[@]}" -gt 0 ]]
55✔
439
        then
440
                LogEnter 'Detected %s modified files in pruned packages.\n' \
×
441
                                 "$(Color G %s "${#modified_files_in_deleted_packages[@]}")"
×
442

443
                LogEnter 'Updating system file data...\n'
×
444
                local file
×
445
                for file in "${modified_files_in_deleted_packages[@]}"
6✔
446
                do
447
                        local system_file="$system_dir"/files"$file"
6✔
448
                        if [[ -h "$system_file" || -f "$system_file" ]]
6✔
449
                        then
450
                                rm --force "$system_file"
×
451
                        elif [[ -d "$system_file" ]]
3✔
452
                        then
453
                                rmdir --ignore-fail-on-non-empty "$system_file"
2✔
454
                        elif [[ -e "$system_file" ]]
1✔
455
                        then
456
                                FatalError '%s exists, but is neither file or directory or link?\n' \
×
457
                                                   "$(Color C "%q" "$file")"
×
458
                        fi
459

460
                        local prop
6✔
461
                        for prop in "${all_file_property_kinds[@]}"
25✔
462
                        do
463
                                printf '%s\t%s\t%q\n' "$prop" '' "$file" >> "$system_dir"/file-props.txt
25✔
464
                        done
465
                done
466
                LogLeave
1✔
467

468
                LogEnter 'Updating owned file list...\n'
1✔
469
                ( "$PACMAN" --query --list --quiet || true ) | sed 's#\/$##' | sort --unique > "$tmp_dir"/owned-files
3✔
470
                LogLeave
1✔
471

472
                LogEnter 'Rescanning...\n'
1✔
473
                AconfAnalyzeFiles
1✔
474
                LogLeave
1✔
475
                LogLeave
1✔
476
        fi
477

478
        #
479
        # Copy files
480
        #
481

482
        LogEnter 'Configuring files...\n'
55✔
483

484
        if [[ ${#changed_files[@]} != 0 ]]
55✔
485
        then
486
                LogEnter 'Overwriting %s changed files.\n' "$(Color G ${#changed_files[@]})"
15✔
487

488
                # shellcheck disable=2059
489
                # shellcheck disable=SC2329  # Callback function invoked indirectly
490
                function Details() {
491
                        Log 'Overwriting the following changed files:\n'
8✔
492
                        printf "$(Color W "*") $(Color C "%s" "%s")\\n" "${changed_files[@]}"
21✔
493
                }
494
                Confirm Details
8✔
495

496
                for file in "${changed_files[@]}"
11✔
497
                do
498
                        LogEnter 'Overwriting %s...\n' "$(Color C "%q" "$file")"
24✔
499
                        ParanoidConfirm Details_DiffFile
12✔
500
                        InstallFile "$file"
12✔
501
                        LogLeave ''
12✔
502
                done
503

504
                modified=y
8✔
505
                LogLeave
8✔
506
        fi
507

508
        if [[ ${#config_only_files[@]} != 0 ]]
56✔
509
        then
510
                LogEnter 'Installing %s new files.\n' "$(Color G ${#config_only_files[@]})"
22✔
511

512
                # shellcheck disable=2059
513
                # shellcheck disable=SC2329  # Callback function invoked indirectly
514
                function Details() {
515
                        Log 'Installing the following new files:\n'
11✔
516
                        printf "$(Color W "*") $(Color C "%s" "%s")\\n" "${config_only_files[@]}"
33✔
517
                }
518
                Confirm Details
11✔
519

520
                for file in "${config_only_files[@]}"
21✔
521
                do
522
                        LogEnter 'Installing %s...\n' "$(Color C "%q" "$file")"
42✔
523
                        ParanoidConfirm ''
21✔
524
                        InstallFile "$file"
21✔
525
                        LogLeave ''
21✔
526
                done
527

528
                modified=y
11✔
529
                LogLeave
11✔
530
        fi
531

532
        local -a files_to_delete=()
112✔
533
        local -a files_to_restore=()
112✔
534

535
        if [[ ${#system_only_files[@]} != 0 ]]
56✔
536
        then
537
                LogEnter 'Processing system-only files...\n'
5✔
538

539
                # Delete unknown stray files (files not present in config and belonging to no package)
540

541
                LogEnter 'Filtering system-only stray files...\n'
5✔
542
                local system_only_stray_files=0
5✔
543
                tr '\n' '\0' < "$tmp_dir"/owned-files > "$tmp_dir"/owned-files-0
5✔
544
                comm -13 --zero-terminated "$tmp_dir"/owned-files-0 <(Print0Array system_only_files) | \
10✔
545
                        while read -r -d $'\0' file
14✔
546
                        do
547
                                files_to_delete+=("$file")
9✔
548
                                system_only_stray_files=$((system_only_stray_files+1))
9✔
549
                        done
550
                LogLeave 'Done (%s system-only stray files).\n' "$(Color G %s $system_only_stray_files)"
10✔
551

552
                # Restore unknown owned files (files not present in config and belonging to a package)
553

554
                LogEnter 'Filtering system-only owned files...\n'
5✔
555
                local system_only_owned_files=0
5✔
556
                comm -12 --zero-terminated "$tmp_dir"/owned-files-0 <(Print0Array system_only_files) | \
10✔
557
                        while read -r -d $'\0' file
10✔
558
                        do
559
                                if [[ "${output_file_props[$file:deleted]:-}" == y ]]
5✔
560
                                then
561
                                        continue # Don't restore files that the user wants deleted
1✔
562
                                fi
563

564
                                files_to_restore+=("$file")
4✔
565
                                system_only_owned_files=$((system_only_owned_files+1))
4✔
566
                        done
567
                LogLeave 'Done (%s system-only owned files).\n' "$(Color G %s $system_only_owned_files)"
10✔
568

569
                LogLeave # Processing system-only files
5✔
570
        fi
571

572
        LogEnter 'Processing deleted files...\n'
56✔
573

574
        if [[ ${#config_only_file_props[@]} != 0 ]]
56✔
575
        then
576
                local key
6✔
577
                for key in "${config_only_file_props[@]}"
33✔
578
                do
579
                        if [[ "$key" == *:deleted ]]
33✔
580
                        then
581
                                local file="${key%:*}"
6✔
582
                                files_to_delete+=("$file")
6✔
583
                                unset "output_file_props[\$key]"
6✔
584
                        fi
585
                done
586
        fi
587

588
        if [[ ${#system_only_file_props[@]} != 0 ]]
56✔
589
        then
590
                local key
3✔
591
                for key in "${system_only_file_props[@]}"
11✔
592
                do
593
                        if [[ "$key" == *:deleted ]]
11✔
594
                        then
595
                                local file="${key%:*}"
3✔
596

597
                                if [[ -h "$output_dir"/files/"$file" || -e "$output_dir"/files/"$file" ]]
5✔
598
                                then
599
                                        # If we are going to replace a deleted file with
600
                                        # one from the configuration, do not attempt to
601
                                        # restore it.
602
                                        :
2✔
603
                                else
604
                                        files_to_restore+=("$file")
1✔
605
                                fi
606

607
                                unset "system_file_props[\$key]"
3✔
608
                        fi
609
                done
610
        fi
611

612
        LogLeave # Processing deleted files
56✔
613

614
        if [[ ${#files_to_delete[@]} != 0 ]]
52✔
615
        then
616
                LogEnter 'Deleting %s files.\n' "$(Color G ${#files_to_delete[@]})"
12✔
617
                printf '%s\0' "${files_to_delete[@]}" | sort --zero-terminated | mapfile -d $'\0' files_to_delete
18✔
618

619
                # shellcheck disable=2059
620
                # shellcheck disable=SC2329  # Callback function invoked indirectly
621
                function Details() {
622
                        Log 'Deleting the following files:\n'
6✔
623
                        printf "$(Color W "*") $(Color C "%s" "%s")\\n" "${files_to_delete[@]}"
18✔
624
                }
625
                Confirm Details
6✔
626

627
                local -A parents=()
12✔
628

629
                # Iterate backwards, so that inner files/directories are
630
                # deleted before their parent ones.
631
                local i
6✔
632
                for (( i=${#files_to_delete[@]}-1 ; i >= 0 ; i-- ))
42✔
633
                do
634
                        local file="${files_to_delete[$i]}"
15✔
635

636
                        if [[ -n "${parents[$file]+x}" && -n "$(sudo find "$file" -maxdepth 0 -type d -not -empty 2>/dev/null)" ]]
29✔
637
                        then
638
                                # Ignoring paths under a directory can cause us to
639
                                # want to remove a directory which will in fact not be
640
                                # empty, and actually contain ignored files. So, skip
641
                                # deleting empty directories which are parents of
642
                                # previously-deleted objects.
643
                                LogEnter 'Skipping non-empty directory %s.\n' "$(Color C "%q" "$file")"
2✔
644
                        else
645
                                LogEnter 'Deleting %s...\n' "$(Color C "%q" "$file")"
28✔
646
                                ParanoidConfirm ''
14✔
647
                                sudo rm --dir "$file"
14✔
648
                        fi
649

650
                        local prop
15✔
651
                        for prop in "${all_file_property_kinds[@]}"
20✔
652
                        do
653
                                local key="$file:$prop"
20✔
654
                                unset "system_file_props[\$key]"
20✔
655
                        done
656

657
                        parents["$(dirname "$file")"]=y
30✔
658

659
                        LogLeave ''
15✔
660
                done
661

662
                modified=y
6✔
663
                LogLeave
6✔
664
        fi
665

666
        if [[ ${#files_to_restore[@]} != 0 ]]
52✔
667
        then
668
                LogEnter 'Restoring %s files.\n' "$(Color G ${#files_to_restore[@]})"
6✔
669
                printf '%s\0' "${files_to_restore[@]}" | sort --zero-terminated | mapfile -d $'\0' files_to_restore
9✔
670

671
                # shellcheck disable=2059
672
                # shellcheck disable=SC2329  # Callback function invoked indirectly
673
                function Details() {
674
                        Log 'Restoring the following files:\n'
3✔
675
                        printf "$(Color W "*") $(Color C "%s" "%s")\\n" "${files_to_restore[@]}"
9✔
676
                }
677
                Confirm Details
3✔
678

679
                # Read file owners
680
                local -A file_owners
3✔
681
                local file
3✔
682
                while read -r -d $'\0' file
164✔
683
                do
684
                        local package
162✔
685
                        read -r -d $'\0' package
161✔
686
                        file_owners[$file]=$package
161✔
687
                done < "$tmp_dir"/file-owners
×
688

689
                for file in "${files_to_restore[@]}"
4✔
690
                do
691
                        local package
4✔
692

693
                        if [[ -n "${file_owners[$file]+x}" ]]
4✔
694
                        then
695
                                package=${file_owners[$file]}
5✔
696
                        else
697
                                package="$( ("$PACMAN" --query --owns --quiet "$file" || true) | head -n 1)"
×
698

699
                                if [[ -z "$package" ]]
×
700
                                then
701
                                        Log 'Can'\''t find package owning file %s\n' "$(Color C "%q" "$file")"
×
702
                                        Exit 1
×
703
                                fi
704
                        fi
705

706
                        LogEnter 'Restoring %s file %s...\n' "$(Color M "%q" "$package")" "$(Color C "%q" "$file")"
15✔
707
                        # shellcheck disable=SC2329  # Callback function invoked indirectly
708
                        function Details() {
709
                                AconfNeedProgram diff diffutils n
1✔
710
                                AconfGetPackageOriginalFile "$package" "$file" | ( "${diff_opts[@]}" --unified <(SuperCat "$file") - || true )
4✔
711
                        }
712
                        if sudo test -f "$file"
5✔
713
                        then
714
                                ParanoidConfirm Details
1✔
715
                        else
716
                                ParanoidConfirm ''
4✔
717
                        fi
718

719
                        AconfRestoreFile "$package" "$file"
5✔
720

721
                        # The file was restored with all of its original properties.
722
                        local prop
5✔
723
                        for prop in owner group mode
15✔
724
                        do
725
                                unset "system_file_props[\$file:\$prop]"
15✔
726
                        done
727

728
                        LogLeave ''
5✔
729
                done
730

731
                modified=y
3✔
732
                LogLeave
3✔
733
        fi
734

735
        #
736
        # Apply remaining file properties
737
        #
738

739
        LogEnter 'Configuring file properties...\n'
52✔
740

741
        AconfCompareFileProps # Update data after ApplyFileProps' unsets
53✔
742

743
        if [[ ${#config_only_file_props[@]} != 0 || ${#changed_file_props[@]} != 0 || ${#system_only_file_props[@]} != 0 ]]
149✔
744
        then
745
                LogEnter 'Found %s new, %s changed, and %s extra files properties.\n'        \
12✔
746
                                 "$(Color G ${#config_only_file_props[@]})"                                                \
12✔
747
                                 "$(Color G ${#changed_file_props[@]})"                                                 \
12✔
748
                                 "$(Color G ${#system_only_file_props[@]})"
12✔
749

12✔
750
                # shellcheck disable=SC2329  # Callback function invoked indirectly
751
                function LogFileProps() {
752
                        local verb="$1"
9✔
753
                        local first=y
9✔
754
                        local key
9✔
755

756
                        while read -r -d $'\0' key
20✔
757
                        do
758
                                if [[ $first == y ]]
11✔
759
                                then
760
                                        LogEnter '%s the following file properties:\n' "$verb"
4✔
761
                                        first=n
4✔
762
                                fi
763

764
                                local kind="${key##*:}"
11✔
765
                                local file="${key%:*}"
11✔
766
                                local value="${output_file_props[$key]:-}"
11✔
767
                                PrintFileProperty "$kind" "$value" "$file"
11✔
768
                        done
769

770
                        if [[ $first == n ]]
9✔
771
                        then
772
                                LogLeave ''
4✔
773
                        fi
774
                }
775

776
                # shellcheck disable=2059
777
                # shellcheck disable=SC2329  # Callback function invoked indirectly
778
                function Details() {
779
                        Print0Array config_only_file_props | LogFileProps "Setting"
6✔
780
                        Print0Array changed_file_props     | LogFileProps "Updating"
6✔
781
                        Print0Array system_only_file_props | LogFileProps "Clearing"
6✔
782
                }
783
                Confirm Details
3✔
784

785
                local key
3✔
786
                ( Print0Array config_only_file_props ; Print0Array changed_file_props ; Print0Array system_only_file_props ) | \
9✔
787
                        while read -r -d $'\0' key
14✔
788
                        do
789
                                local kind="${key##*:}"
11✔
790
                                local file="${key%:*}"
11✔
791
                                local value="${output_file_props[$key]:-}"
11✔
792

793
                                ApplyFileProperty "$kind" "$value" "$file"
11✔
794
                        done
795

796
                modified=y
3✔
797
                LogLeave
3✔
798
        fi
799

800
        LogLeave # Configuring file properties
51✔
801

802
        LogLeave # Configuring files
50✔
803

804
        if [[ $modified == n ]]
50✔
805
        then
806
                LogLeave 'Done (%s).\n' "$(Color G "system state unchanged")"
15✔
807
        else
808
                LogLeave 'Done (%s).\n' "$(Color Y "system state changed")"
89✔
809
        fi
810
}
811

812
: # include in coverage
90✔
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