File: ch-completion.bash

package info (click to toggle)
charliecloud 0.43-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,116 kB
  • sloc: python: 6,021; sh: 4,284; ansic: 3,863; makefile: 598
file content (982 lines) | stat: -rw-r--r-- 35,409 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
# Completion script for Charliecloud

# SC2207 pops up whenever we do “COMPREPLY=( $(compgen [...]) )”. This seems to
# be standard for implementations of bash completion, and we didn't like the
# suggested alternatives, so we disable it here.
# shellcheck disable=SC2207

# SC2034 complains about modifying variables by reference in
# _ch_run_parse. Disable it.
# shellcheck disable=SC2034

# Permissions for this file:
#
# This file needs to be sourced, not executed. Because of this, the execute bit
# for the file should remain unset for all permission groups.
#
# (sourcing versus executing: https://superuser.com/a/176788)

# Resources for understanding this script:
#
#   * Everything bash:
#     https://www.gnu.org/software/bash/manual/html_node/index.html
#
#   * Bash parameter expansion:
#     https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
#
#   * Bash completion builtins (compgen, comopt, etc.):
#     https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html
#
#   * Bash completion variables (e.g. COMPREPLY):
#     https://devmanual.gentoo.org/tasks-reference/completion/index.html
#
#   * Call-by-reference for bash function args:
#     https://unix.stackexchange.com/a/224564


## SYNTAX GLOSSARY ##
#
# Bash has some pretty unusual syntax, and this script has no shortage of
# strange Bash-isms. I’m including this syntax glossary with the hope that it’ll
# make this code more readable for Bash newbies and those who are rusty. For more
# info, see the gnu.org “Bash parameter expansion” page linked above, which is also
# the source for this glossary.
#
# ${array[i]}
#   Gives the ith element of “array”. Note that bash arrays are indexed at
#   zero, as all things should be.
#
# ${array[@]}
#   Expands “array” to its member elements as a sequence of words, one word
#   per element.
#
# ${#parameter}
#   Gives the length of “parameter”. If “parameter” is a string, this
#   expansion gives you the character length of the string. If “parameter” is
#   an array subscripted by “@” or “*” (e.g. “foo[@]”), then the expansion
#   gives you the number of elements in the array.
#
# ${parameter:offset:length}
#   A.k.a. substring expansion. If “parameter” is a string, expand up to
#   “length” characters, starting with the character at position “offset.” If
#   “offset” is unspecified, start at the first character. If “parameter” is
#   an array subscripted by “@” or “*,” (e.g. “foo[@]”) expand up to “length”
#   elements, starting at the element at position “offset” (e.g.
#   “${foo[offset]}”).
#
#   Example 1 (string):
#
#     $ foo="abcdef"
#     $ echo ${foo::3}
#     abc
#     $ echo ${foo:1:3}
#     bcd
#
#   Example 2 (array):
#
#     $ foo=("a" "b" "c" "d" "e" "f")
#     $ echo ${foo[@]::3}
#     a b c
#     $ echo ${foo[@]:1:3}
#     b c d
#
# ${parameter/pattern/string}
#   This is a form of pattern replacement in which “parameter” is expanded and
#   the first instance of “pattern” is replaced with “string”.
#
# ${parameter//pattern/string}
#   Similar to “${parameter/pattern/string}” above, except every instance of
#   “pattern” in the expanded parameter is replaced by “string” instead of only
#   the first.
#


## Setup ##

# According to this post (https://stackoverflow.com/a/50281697), Bash 4.3 alpha
# added the feature that enables the use of out parameters for functions (or
# passing variables by reference), which is an integral feature of this script.
bash_vmin=4.3.0

# Check Bash version
bash_v=$(bash --version | head -1 | grep -Eo "[0-9\.]{2,}[0-9]")
if [[ $(printf "%s\n%s\n" "$bash_vmin" "$bash_v" | sort -V | head -1) != "$bash_vmin" ]]; then
    echo "ch-completion.bash: unsupported bash version ($bash_v < $bash_vmin)"
    return 1
fi

# Check for bash completion, exit if not found. FIXME: #1640.
if [[ -z "$(declare -f -F _get_comp_words_by_ref)" ]]; then
    if [[ -f /usr/share/bash-completion/bash_completion ]]; then
        . /usr/share/bash-completion/bash_completion
    else
        echo "ch-completion.bash: dependency \"bash_completion\" not found, exiting"
        return 1
    fi
fi

# https://stackoverflow.com/a/246128
_ch_completion_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
_ch_completion_version="$("$_ch_completion_dir"/../misc/version)"

_ch_completion_log="/tmp/ch-completion.log"

# Record file being sourced.
if [[ -n "$CH_COMPLETION_DEBUG" ]]; then
    printf "ch-completion.bash sourced\n\n" >> "$_ch_completion_log"
fi

_ch_completable_executables="ch-image ch-run ch-convert"


## ch-convert ##

# Valid formats
_convert_fmts="ch-image dir docker podman squash tar"

# Options for ch-convert that accept args
_convert_arg_opts="-i --in-fmt -o --out-fmt -s --storage --tmp"

# All options for ch-convert
_convert_opts="-h --help -n --dry-run --no-clobber --no-xattrs -v --verbose
               $_convert_arg_opts"


# Completion function for ch-convert
#
_ch_convert_complete () {
    local prev
    local cur
    local fmt_in
    local fmt_out
    local words
    local opts_end=-1
    local strg_dir
    local extras
    _get_comp_words_by_ref -n : cur prev words cword

    strg_dir=$(_ch_find_storage "${words[@]::${#words[@]}-1}")
    _ch_convert_parse "$strg_dir" "$cword" fmt_in fmt_out opts_end "${words[@]}"

    # Populate debug log
    _DEBUG "\$ ${words[*]}"
    _DEBUG " storage: dir: $strg_dir"
    _DEBUG " current: $cur"
    _DEBUG " previous: $prev"
    _DEBUG " input format: $fmt_in"
    _DEBUG " output format: $fmt_out"
    if [[ $opts_end != -1 ]]; then
        _DEBUG " input image: ${words[$opts_end]}"
    else
        _DEBUG " input image:"
    fi

    # Command line options
    if [[ ($opts_end == -1) || ($cword -lt $opts_end) ]]; then
        case "$prev" in
        -i|--in-fmt)
            COMPREPLY=( $(compgen -W "${_convert_fmts//$fmt_out/}" -- "$cur") )
            return 0
            ;;
        -o|--out-fmt)
            COMPREPLY=( $(compgen -W "${_convert_fmts//$fmt_in/}" -- "$cur") )
            return 0
            ;;
        -s|--storage|--tmp)
            # Avoid overzealous completion. E.g. if there’s only one subdir of the
            # current dir, this command completes to that dir even if $cur is empty
            # (i.e. the user hasn’t yet typed anything), which seems confusing for
            # the user.
            if [[ -n "$cur" ]]; then
                compopt -o nospace
                COMPREPLY=( $(compgen -d -S / -- "$cur") )
            fi
            return 0
            ;;
        *)
            # Not an option that requires an arg.
            COMPREPLY=( $(compgen -W "$_convert_opts" -- "$cur") )
            ;;
        esac
    fi

    if [[ ($opts_end == -1) ]]; then
        # Input image not yet specified, complete potential input images.
        case "$fmt_in" in
        ch-image)
            COMPREPLY+=( $(compgen -W "$(_ch_list_images "$strg_dir")" -- "$cur") )
            __ltrim_colon_completions "$cur"
            ;;
        dir)
            COMPREPLY+=( $(compgen -d -- "$cur") )
            if [[ -n "$(compgen -d -- "$cur")" ]]; then
                compopt -o nospace
            fi
            return 0
            ;;
        squash)
            COMPREPLY+=( $(_compgen_filepaths -X "!*.sqfs" "$cur") )
            _space_filepath -X "!*.sqfs" "$cur"
            ;;
        tar)
            COMPREPLY+=( $(_compgen_filepaths -X "!*.tar.* !*tgz" "$cur") )
            _space_filepath -X "!*.tar.* !*tgz" "$cur"
            ;;
        docker|podman)
            # We don’t attempt to complete in this case.
            return 0
            ;;
        "")
            # No in fmt specified, could be anything
            COMPREPLY+=( $(_compgen_filepaths -X "!*.tar.* !*tgz !*.sqfs" "$cur") )
            COMPREPLY+=( $(compgen -W "$(_ch_list_images "$strg_dir")" -- "$cur") )
            _space_filepath -X "!*.tar.* !*tgz !*.sqfs" "$cur"
            __ltrim_colon_completions "$cur"
            return 0
            ;;
        esac
    elif [[ ($cword -gt $opts_end) ]]; then
        # Input image has been specified and current word appears after it in
        # the command line. Assume we’re completing output image. If output
        # format COULD be dir, tar, or squash, complete valid directory paths.
        if ! _is_subword "$fmt_out" "ch-image docker podman"; then
            compopt -o nospace
            COMPREPLY+=( $(compgen -d -S / -- "$cur") )
        fi
        return 0
    fi

    return 0
}

## ch-image ##

# Subcommands and options for ch-image
#

_image_build_opts="-b --bind --build-arg -f --file --force
                   --force-cmd -n --dry-run --parse-only -t --tag"

_image_modify_opts="-c -S --shell"

_image_common_opts="-a --arch --always-download --auth --break
                    --cache --cache-large --dependencies -h --help
                    --no-cache --no-lock --no-xattrs --profile
                    --rebuild --password-many -q --quiet -s --storage
                    --tls-no-verify -v --verbose --version --xattrs"

_image_subcommands="build build-cache delete gestalt import
                    list modify pull push reset undelete"

# archs taken from ARCH_MAP in charliecloud.py
_archs="amd64 arm/v5 arm/v6 arm/v7 arm64/v8 386 mips64le ppc64le s390x"

# Completion function for ch-image
#
_ch_image_complete () {
    local prev
    local cur
    local cword
    local words
    local sub_cmd
    local strg_dir
    local extras=
    _get_comp_words_by_ref -n : cur prev words cword

    sub_cmd=$(_ch_image_subcmd_get "$cword" "${words[@]}")
    # To find the storage directory, we want to look at all the words in the
    # current command line except for the current word (“${words[$cword]}”
    # here). We do this to prevent unexpected behavior resulting from the
    # current word being incomplete. The bash syntax we use to accomplish this
    # is “"${array[@]::$i}" "${array[@]:$i+1:${#array[@]}-1}"” which is
    # analogous to “array[:i] + array[i+1:]” in Python, giving you all elements
    # of the array, except for the one at index “i”. The syntax glossary at the
    # top of this file gives a breakdown of the constituent elements of this
    # hideous expression.
    strg_dir=$(_ch_find_storage "${words[@]::$cword}" "${words[@]:$cword+1:${#array[@]}-1}")

    # Populate debug log
    _DEBUG "\$ ${words[*]}"
    _DEBUG " storage: dir: $strg_dir"
    _DEBUG " word index: $cword"
    _DEBUG " current: $cur"
    _DEBUG " previous: $prev"
    _DEBUG " sub command: $sub_cmd"

    # Common opts that take args
    #
    case "$prev" in
    -a|--arch)
        COMPREPLY=( $(compgen -W "host yolo $_archs" -- "$cur") )
        return 0
        ;;
    --break)
        # “--break” arguments take the form “MODULE:LINE”. Complete “MODULE:”
        # from python files in lib (we can’t complete line number).
        COMPREPLY=( $(compgen -S : -W "$(_compgen_py_libfiles)" -- "$cur") )
        __ltrim_colon_completions
        compopt -o nospace
        return 0
        ;;
    --cache-large)
        # This is just a user-specified number. Can’t autocomplete
        COMPREPLY=()
        return 0
        ;;
    -s|--storage)
        # See comment about overzealous completion for the “--storage” option
        # under “_ch_convert_complete”.
        if [[ -n "$cur" ]]; then
            compopt -o nospace
            COMPREPLY=( $(compgen -d -S / -- "$cur") )
        fi
        return 0
        ;;
    esac

    case "$sub_cmd" in
    build)
        case "$prev" in
        # Go through a list of potential subcommand-specific opts to see if
        # $cur should be an argument. Otherwise, default to CONTEXT or any
        # valid option (common or subcommand-specific).
        -f|--file)
            COMPREPLY=( $(_compgen_filepaths "$cur") )
            _space_filepath "$cur"
            return 0
            ;;
        -t)
            # We can’t autocomplete a tag, so we're not even gonna try.
            COMPREPLY=()
            return 0
            ;;
        *)
            # Autocomplete to context directory, common opt, or build-specific
            # opt --force can take “fakeroot” or “seccomp” as an argument, or
            # no argument at all.
            if [[ $prev == --force ]]; then
                extras+="$extras fakeroot seccomp"
            fi
            COMPREPLY=( $(compgen -W "$_image_build_opts $extras"  -- "$cur") )
            # By default, “complete” adds a space after each completed word.
            # This is incredibly inconvenient when completing directories and
            # filepaths, so we enable the “nospace” option. We want to make
            # sure that this option is only enabled if there are valid path
            # completions for $cur, otherwise spaces would never be added
            # after a completed word, which is also inconveninet.
            if [[ -n "$(compgen -d -S / -- "$cur")" ]]; then
                compopt -o nospace
                COMPREPLY+=( $(compgen -d -S / -- "$cur") )
            fi
            ;;
        esac
        ;;
    build-cache)
        COMPREPLY=( $(compgen -W "--reset --gc --tree --dot" -- "$cur") )
        ;;
    delete|list|modify)
        case "$sub_cmd" in
        list)
            if [[ "$prev" == "--undeletable" || "$prev" == "--undeleteable" || "$prev" == "-u" ]]; then
                COMPREPLY=( $(compgen -W "$(_ch_undelete_list "$strg_dir")" -- "$cur") )
                return 0
            fi
            extras="$extras -l --long -u --undeletable"
            # If “cur” starts with “--undelete,” add “--undeleteable” (the less
            # correct version of “--undeletable”) to the list of possible
            # completions.
            if [[ ${cur::10} == "--undelete" ]]; then
                extras="$extras --undeleteable"
            fi
            ;;
        modify)
            # FIXME: Implement
            extras="$extras $_image_modify_opts"
            ;;
        esac
        COMPREPLY=( $(compgen -W "$(_ch_list_images "$strg_dir") $extras" -- "$cur") )
        __ltrim_colon_completions "$cur"
        ;;
    gestalt)
        COMPREPLY=( $(compgen -W "bucache bucache-dot python-path
                                  storage-path" -- "$cur") )
        ;;
    import)
        # Complete (1) directories and (2) files named like tarballs.
        COMPREPLY+=( $(_compgen_filepaths -X "!*.tar.* !*tgz" "$cur") )
        if [[ ${#COMPREPLY} -gt 0 ]]; then
            compopt -o nospace
        fi
        ;;
    push)
        if [[ "$prev" == "--image" ]]; then
            compopt -o nospace
            COMPREPLY=( $(compgen -d -S / -- "$cur") )
            return 0
        fi
        COMPREPLY=( $(compgen -W "$(_ch_list_images "$strg_dir") --image" -- "$cur") )
        __ltrim_colon_completions "$cur"
        ;;
    undelete)
        COMPREPLY=( $(compgen -W "$(_ch_undelete_list "$strg_dir")" -- "$cur") )
        ;;
    '')
        # Only autocomplete subcommands if there's no subcommand present.
        COMPREPLY=( $(compgen -W "$_image_subcommands" -- "$cur") )
        ;;
    esac

    # If we’ve made it this far, the last remaining option for completion is
    # common opts.
    COMPREPLY+=( $(compgen -W "$_image_common_opts" -- "$cur") )
    return 0
}


## ch-run ##

# Options for ch-run
#

_run_opts="-b --bind -c --cd --env-no-expand --feature -g --gid
           --home -j --join --join-pid --join-ct --join-tag -m
           --mount --no-passwd -q --quiet -s --storage --seccomp -t
           --private-tmp --set-env -u --uid --unsafe --unset-env
           -v --verbose -w --write -? --help --usage -V --version"

_run_features="extglob seccomp squash" # args for the --feature option

# Completion function for ch-run
#
_ch_run_complete () {
    local prev
    local cur
    local cword
    local words
    local strg_dir
    local extras=
    _get_comp_words_by_ref -n : cur prev words cword

    # See the comment above the first call to “_ch_find_storage” for an
    # explanation of the horrible syntax here.
    strg_dir=$(_ch_find_storage "${words[@]::$cword}" "${words[@]:$cword+1:${#array[@]}-1}")
    local cli_image
    local cmd_index=-1
   _ch_run_parse "$strg_dir" "$cword" cli_image cmd_index "${words[@]}"

    # Populate debug log
    _DEBUG "\$ ${words[*]}"
    _DEBUG " storage: dir: $strg_dir"
    _DEBUG " word index: $cword"
    _DEBUG " current: $cur"
    _DEBUG " previous: $prev"
    _DEBUG " cli image: $cli_image"

    # Currently, we don’t try to suggest completions if you’re in the “command”
    # part of the ch-run CLI (i.e. entering commands to be run inside the
    # container). Implementing this *may* be possible, but doing so would likely
    # be absurdly complicated, so we don’t plan on it.
    if [[ $cmd_index != -1 && $cmd_index -lt $cword ]]; then
        COMPREPLY=()
        return 0
    fi

    # Common opts that take args
    #
    case "$prev" in
    -b|--bind)
        COMPREPLY=()
        return 0
        ;;
    -c|--cd)
        COMPREPLY=()
        return 0
        ;;
    --feature)
        COMPREPLY=( $(compgen -W "$_run_features" --  "$cur") )
        return 0
        ;;
    -g|--gid)
        COMPREPLY=()
        return 0
        ;;
    --join-pid)
        COMPREPLY=()
        return 0
        ;;
    --join-ct)
        COMPREPLY=()
        return 0
        ;;
    --join-tag)
        COMPREPLY=()
        return 0
        ;;
    -m|--mount)
        compopt -o nospace
        COMPREPLY=( $(compgen -d -- "$cur") )
        return 0
        ;;
    -s|--storage)
        # See comment about overzealous completion for the “--storage” option
        # under “_ch_convert_complete”.
        if [[ -n "$cur" ]]; then
            compopt -o nospace
            COMPREPLY=( $(compgen -d -S / -- "$cur") )
        fi
        return 0
        ;;
    --set-env)
        extras+=$(compgen -f -- "$cur")
        ;;
    -u|--uid)
        COMPREPLY=()
        return 0
        ;;
    --unset-env)
        COMPREPLY=()
        return 0
        ;;
    esac

    if [[ -z $cli_image ]]; then
        # No image found in command line, complete dirs, tarfiles, and sqfs
        # archives
        COMPREPLY=( $(_compgen_filepaths -X "!*.sqfs" "$cur") )
        # Complete images in storage. Note we don't use “ch-image list” here
        # because it can initialize an empty storage directory and we don't want
        # this script to have any such side effects.
        COMPREPLY+=( $(compgen -W "$(_ch_list_images "$strg_dir")" -- "$cur") )
        __ltrim_colon_completions "$cur"
    fi

    _space_filepath -X "!*.sqfs" "$cur"
    COMPREPLY+=( $(compgen -W "$_run_opts $extras" -- "$cur") )
    return 0
}


## Helper functions ##

_ch_completion_help="Usage: ch-completion [ OPTION ]

Utility function for Charliecloud tab completion.

    --disable     disable tab completion for all Charliecloud executables
    --help        show this help message
    --version     check tab completion script version
    --version-ok  check version compatibility between tab completion and Charliecloud
                  executables
"

# Add debugging text to log file if CH_COMPLETION_DEBUG is specified.
_DEBUG () {
    if [[ -n "$CH_COMPLETION_DEBUG" ]]; then
        #echo "$@" >> "$_ch_completion_log"
        printf "%s\n" "$@" >> "$_ch_completion_log"
    fi
}

# Utility function for Charliecloud tab completion that’s available to users.
ch-completion () {
    while true; do
        case $1 in
            --disable)
                complete -r ch-convert
                complete -r ch-image
                complete -r ch-run
                ;;
            --help)
                printf "%s" "$_ch_completion_help" 1>&2
                return 0
                ;;
            --version)
                printf "%s\n" "$_ch_completion_version" 1>&2
                ;;
            --version-ok)
                if _version_ok_ch_completion "ch-image"; then
                    printf "version ok\n" 1>&2
                    return 0
                else
                    printf "ch-image:      %s\n" "$(ch-image --version)" 1>&2
                    printf "ch-completion: %s\n" "$_ch_completion_version" 1>&2
                    printf "version incompatible!\n" 1>&2
                    return 1
                fi
                ;;
            *)
                break
                ;;
        esac
        shift
    done
}

_completion_opts="--disable --help --version --version-ok"

# Yes, the utility function needs completion too...
#
_ch_completion_complete () {
    local cur
    _get_comp_words_by_ref -n : cur

    COMPREPLY=( $(compgen -W "$_completion_opts" -- "$cur") )
    return 0
}

# Parser for ch-convert command line. Takes 6 arguments:
#
#   1.) A string representing the path to the storage directory.
#
#   2.) The current position (measured in words) of the cursor in the array
#       representing the command line (index starting at 0).
#
#   3.) An out parameter (explanation below). If “_ch_convert_parse” is able to
#       determine the format of the input image, it will pass that format back
#       to the caller as a string using this out parameter. There are two ways
#       that “_ch_convert_parse” can determine the input image format:
#           i.) If “-i” or “--in-fmt” is specified and is followed by a valid
#               image format, the out parameter will be set to a that format.
#               E.g. “ch-image”.
#           ii.) If the parser detects that an input image has been specified,
#                it will try to determine the format of that image. This does
#                not work for Docker or Podman images, and never will.
#
#   4.) Another out parameter. If the user has specified an output image format
#       using “-o” or “--out-fmt”, the parser will use this out parameter to
#       pass that format back to the caller.
#
#   5.) A string representing the expanded command line array (i.e.
#       "${array[@]}").
#
# “Out parameter” here refers to a variable that is meant to pass information
# from this function to its caller (here the “_ch_chonvert_complete” function).
# Out parameters should be passed to a bash function as the unquoted names of
# variables (e.g. “var” instead of “$var” or “"$var"”) within the caller’s
# scope. Passing the variables to the function in this way allows it to change
# their values, and for those changes to persist in the scope that called the
# function (this is what makes them “out parameters”).
#
_ch_convert_parse () {
    local images
    images=$(_ch_list_images "$1")
    local cword="$2"
    local -n in_fmt=$3
    local -n out_fmt=$4
    local -n end_opts=$5
    shift 5
    local words=("$@")
    local ct=1

    while ((ct < ${#words[@]})); do
        case ${words[$ct-1]} in
        -i|--in-fmt)
            if _is_subword "${words[$ct]}" "$_convert_fmts"; then
                in_fmt="${words[$ct]}"
            fi
            ;;
        -o|--out-fmt)
            if _is_subword "${words[$ct]}" "$_convert_fmts"; then
                out_fmt="${words[$ct]}"
            fi
            ;;
        esac

        if (! _is_subword "${words[$ct-1]}" "$_convert_arg_opts") \
          &&  [[ ("${words[$ct]}" != "-"*) && ($ct -ne $cword) ]]; then
            # First non-opt arg found, assuming it’s the input image
            end_opts=$ct
            local word
            word="$(_sanitized_tilde_expand "${words[$ct]}")"
            if [[ -z "$in_fmt" ]]; then
                # If the parser hasn’t been told the input image format yet, try
                # to figure it out from available information.
                if _is_subword "${words[$ct]}" "$images"; then
                    # Check for storage images first because this is what
                    # ch-convert seems to default to in the case of a name
                    # collision between different image formats (e.g. if “foo”
                    # is an image in storage and “./foo/” is in the working
                    # directory).
                    in_fmt="ch-image"
                elif [[ -d "$word" ]]; then
                    in_fmt="dir"
                elif [[ -f "$word" ]]; then
                    if [[ ("${words[$ct]}" == *".tgz") || ("${words[$ct]}" == *".tar."*) ]]; then
                        in_fmt="tar"
                    elif [[ "${words[$ct]}" == *".sqfs" ]]; then
                        in_fmt="squash"
                    fi
                fi
            fi
        fi

        ((ct++))
    done
}

# Figure out which storage directory to use (including cli-specified storage).
# Remove trailing slash. Note that this isn't performed when the script is
# sourced because the working storage directory can effectively change at any
# time with “CH_IMAGE_STORAGE” or the “--storage” option.
_ch_find_storage () {
    if echo "$@" | grep -Eq -- '\s(--storage|-\w*s)'; then
        # This if “--storage” or “-s” are in the command line.
        sed -Ee 's/(.*)(--storage=*|[^-]-s=*)\ *([^ ]*)(.*$)/\3/g' -Ee 's|/$||g' <<< "$@"
    elif [[ -n "$CH_IMAGE_STORAGE" ]]; then
        echo "$CH_IMAGE_STORAGE" | sed -Ee 's|/$||g'
    else
        echo "/var/tmp/$USER.ch"
    fi
}

# Print the subcommand in an array of words; if there is not one, print an empty
# string. This feels a bit kludge-y, but it's the best I could come up with.
# It's worth noting that the double for loop doesn't take that much time, since
# the Charliecloud command line, even in the wost case, is relatively short.
#
# Usage: _ch_image_subcmd_get [words]
#
# Example:
#   >> _ch_image_subcmd_get "ch-image [...] build [...]"
#   build
_ch_image_subcmd_get () {
    local cword="$1"
    shift 1
    local subcmd
    local wrds=("$@")
    local ct=1

    while ((ct < ${#wrds[@]})); do
        if [[ $ct -ne $cword ]]; then
            for subcmd_i in $_image_subcommands; do
                if [[ "${wrds[$ct]}" == "$subcmd_i" ]]; then
                    subcmd="$subcmd_i"
                    break 2
                fi
            done
        fi
        ((ct++))
    done
    echo "$subcmd"
}

# List images in storage directory.
_ch_list_images () {
    # “find” throws an error if “img” subdir doesn't exist or is empty, so check
    # before proceeding.
    if [[ -d "$1/img" && -n "$(ls -A "$1/img")" ]]; then
        find "$1/img/"* -maxdepth 0 -printf "%f\n" | sed -e 's|+|:|g' -e 's|%|/|g'
    fi
}

# Horrible, disgusting function to find an image or image ref in the ch-run
# command line. This function takes five arguments:
#
#   1.) A string representing the path to the storage directory.
#
#   2.) The current position (measured in words) of the cursor in the array
#       representing the command line (index starting at 0).
#
#   3.) An out parameter (see explanation above “_ch_convert_parse”). If
#       “_ch_run_parse” finds the name of an image in storage (e.g.
#       “alpine:latest”) or something that looks like an image path (i.e. a
#       directory, tarball or file named like a squash archive) in the command
#       line, the value of the variable will be updated to the image name or
#       path. If neither are found, the function will not modify the value of
#       this variable.
#
#   4.) Another out parameter. If this function finds “--” in the current
#       command line and it doesn't seem like the user is trying to complete
#       that “--” to an option, “_ch_run_parse” will assume that this is the
#       point beyond which the user specifies commands to be run inside the
#       container and will give the variable the index value of the “--”. Our
#       criterion for deciding that the user isn't trying to complete “--” to an
#       option is that the current index of the cursor in the word array
#       (argument 2, see above) is not equal to the position of the “--” in said
#       array.
#
#   5.) A string representing the expanded command line array (i.e.
#       "${array[@]}").
#
_ch_run_parse () {
    # The essential purpose of this function is to try to find an image in the
    # current command line. If it finds one, it passes the “name” of the image
    # back to the caller in the form of an out parameter (see above). If it
    # doesn't find one, the out parameter remains unmodified. This function
    # assumes that the out parameter in question is the empty string before it
    # gets called.
    local images                   # these two lines are separate b/c SC2155
    images=$(_ch_list_images "$1") #
    shift 1
    local cword="$1"
    shift 1
    local -n cli_img=$1
    local -n cmd_pt=$2
    shift 2
    local wrds=("$@")
    local ct=1

    # Check for tarballs and squashfs archives.
    while ((ct < ${#wrds[@]})); do
        # In bash, expansion of the “~” character to the value of $HOME doesn't
        # happen if a value is quoted (see
        # https://stackoverflow.com/a/52519780). To work around this, we add
        # “eval echo” (https://stackoverflow.com/a/6988394) to this test.
        if [[ $ct != "$cword" ]]; then
            if [[    (    -f "$(_sanitized_tilde_expand "${wrds[$ct]}")" \
                    && (       ${wrds[$ct]} == *.sqfs \
                            || ${wrds[$ct]} == *.tar.? \
                            || ${wrds[$ct]} == *.tar.?? \
                            || ${wrds[$ct]} == *.tgz ) ) \
                || (    -d ${wrds[$ct]} \
                    && ${wrds[$ct-1]} != --mount \
                    && ${wrds[$ct-1]} != -m \
                    && ${wrds[$ct-1]} != --bind \
                    && ${wrds[$ct-1]} != -b \
                    && ${wrds[$ct-1]} != -c \
                    && ${wrds[$ct-1]} != --cd ) ]]; then
                cli_img="${wrds[$ct]}"
            fi
            if [[ ${wrds[$ct]} == "--" ]]; then
                cmd_pt=$ct
            fi
            # Check for refs to images in storage.
            if [[ -z $cli_img ]]; then
                if _is_subword "${wrds[$ct]}" "$images"; then
                    cli_img="${wrds[$ct]}"
                fi
            fi
        fi
        ((ct++))
    done
}

# List undeletable images in the build cache, if it exists.
_ch_undelete_list () {
    if [[ -d "$1/bucache/" ]]; then
        git -C "$strg_dir/bucache/" tag -l | sed -e "s/&//g" \
                                                 -e "s/%/\//g" \
                                                 -e "s/+/:/g"
    fi
}

# Returns filenames and directories, appending a slash to directory names.
# This function takes option “-X”, a string of space-separated glob patterns
# to be excluded from file completion using the compgen option of the same
# name (source: https://stackoverflow.com/a/40227233, see also:
# https://devdocs.io/bash/programmable-completion-builtins#index-compgen)
_compgen_filepaths () {
    local filterpats=("")
    if [[ "$1" == "-X" && 1 -lt ${#@} ]]; then
        # Read a string into an array:
        #   https://stackoverflow.com/a/10586169
        # Pitfalls:
        #   https://stackoverflow.com/a/45201229
        # FIXME: Need to modify $IFS before doing this?
        read -ra filterpats <<< "$2"
        shift 2
    fi

    local cur="$1"

    # Files, excluding directories, with no trailing slashes. The grep
    # performs an inverted substring match on the list of directories and the
    # list of files respectively produced by compgen. The compgen statements
    # also prepend (-P) a “^” and append (-S) a “$” to the file/dir names to
    # avoid the case where a substring matching a dirname is erroniously
    # removed from a filename by the inverted match. These delimiters are then
    # removed by the “sed”. (See the StackOverflow post cited above for OP’s
    # explanation of this code). The for loop iterates through exclusion
    # patterns specified by the “-X” option. If “-X” isn't specified, the code
    # in the loop executes once, with no patterns excluded (“-X ""”).
    for pat in "${filterpats[@]}"
    do
        grep -v -F -f <(compgen -d -P ^ -S '$' -X "$pat" -- "$cur") \
            <(compgen -f -P ^ -S '$' -X "$pat" -- "$cur") |
            sed -e 's/^\^//' -e 's/\$$/ /' \
                -e 's/ $//g'               # remove trailing space
    done

    # Directories with trailing slashes:
    compgen -d -S / -- "$cur"
}

# Wrapper for a horrible pipeline to complete python files in lib.
_compgen_py_libfiles () {
    compgen -f "$_ch_completion_dir/../lib/" |
        grep -o -E ".*\.py" |
        sed "s|$_ch_completion_dir\/\.\.\/lib\/\(.*\)\.py|\1|"
}

# Return 0 if "$1" is a word in space-separated sequence of words "$2", e.g.
#
#   >>> _is_subword "foo" "foo bar baz"
#   0
#   >>> _is_subword "foo" "foobar baz"
#   1
#
_is_subword () {
    local subword=$1
    shift 1
    #shellcheck disable=SC2068
    for word in $@; do
        if [[ "$word" == "$subword" ]]; then
            return 0
        fi
    done
    return 1
}

# Expand tilde in quoted strings to the correct home path, if applicable, while
# sanitizing to prevent code injection (see https://stackoverflow.com/a/38037679).
#
_sanitized_tilde_expand () {
    if [[ $1 == ~* ]]; then
        # Adding the “/” at the end here is important for ensuring that the “~”
        # always gets expanded, e.g. in the case where "$1" is “~” instead of
        # “~/”.
        user="$(echo "$1/" | sed -E 's|^~([^~/]*/).*|\1|g')"
        path="$(echo "$1" | sed -E 's|^~[^~/]*(.*)|\1|g')"
        eval "$(printf "home=~%q" "$user")"
        # Check if “home” is a valid directory.
        # shellcheck disable=SC2154
        if [[ -d "$home" ]]; then
            # The first character of “path” is “/”. Since we've added a “/” to
            # the end of “home” for proper “~” expansion, we now avoid the first
            # character of “path” in the concatenation of the two to avoid a
            # “//”.
            echo "$home${path:1:${#path}-1}"
            return 0
        fi
    fi
    echo "$1"
}

# Wrapper for some tricky logic that determines whether or not to add a space at
# the end of a path completion. For the sake of convenience we want to avoid
# adding a space at the end if the completion is a directory path, because we
# don’t know if the user is looking for the completed directory or one of its
# subpaths (we may be able to figure this out in some cases, but I’m not gonna
# worry about that now). We *do* want to add a space at the end if the
# completion is the path to a file.
_space_filepath () {
    local files
    files="$(_compgen_filepaths "$1" "$2" "$3")"
    if [[ (-n "$files") \
         && (! -f "$(_sanitized_tilde_expand "$files")") ]]; then
        compopt -o nospace
    fi
}

_version_ok_ch_completion () {
    if [[ "$($1 --version 2>&1)" == "$_ch_completion_version" ]]; then
        return 0
    else
        return 1
    fi
}

complete -F _ch_completion_complete ch-completion
complete -F _ch_convert_complete ch-convert
complete -F _ch_image_complete ch-image
complete -F _ch_run_complete ch-run