usage/complete/
bash.rs

1use crate::complete::CompleteOptions;
2use heck::ToSnakeCase;
3
4pub fn complete_bash(opts: &CompleteOptions) -> String {
5    let usage_bin = &opts.usage_bin;
6    let bin = &opts.bin;
7    let bin_snake = bin.to_snake_case();
8    let spec_variable = if let Some(cache_key) = &opts.cache_key {
9        format!("_usage_spec_{bin_snake}_{}", cache_key.to_snake_case())
10    } else {
11        format!("_usage_spec_{bin_snake}")
12    };
13    let mut out = vec![];
14    if opts.include_bash_completion_lib {
15        out.push(include_str!("../../bash-completion/bash_completion").to_string());
16        out.push("\n".to_string());
17    };
18    out.push(format!(
19        r#"_{bin_snake}() {{
20    if ! command -v {usage_bin} &> /dev/null; then
21        echo >&2
22        echo "Error: {usage_bin} CLI not found. This is required for completions to work in {bin}." >&2
23        echo "See https://usage.jdx.dev for more information." >&2
24        return 1
25    fi"#));
26
27    if let Some(usage_cmd) = &opts.usage_cmd {
28        out.push(format!(
29            r#"
30    if [[ -z ${{{spec_variable}:-}} ]]; then
31        {spec_variable}="$({usage_cmd})"
32    fi"#
33        ));
34    }
35
36    if let Some(spec) = &opts.spec {
37        out.push(format!(
38            r#"
39    read -r -d '' {spec_variable} <<'__USAGE_EOF__'
40{spec}
41__USAGE_EOF__"#,
42            spec = spec.to_string().trim()
43        ));
44    }
45
46    out.push(format!(
47        r#"
48	local cur prev words cword was_split comp_args
49    _comp_initialize -n : -- "$@" || return
50    # shellcheck disable=SC2207
51	_comp_compgen -- -W "$({usage_bin} complete-word --shell bash -s "${{{spec_variable}}}" --cword="$cword" -- "${{words[@]}}")"
52	_comp_ltrim_colon_completions "$cur"
53    # shellcheck disable=SC2181
54    if [[ $? -ne 0 ]]; then
55        unset COMPREPLY
56    fi
57    return 0
58}}
59
60if [[ "${{BASH_VERSINFO[0]}}" -eq 4 && "${{BASH_VERSINFO[1]}}" -ge 4 || "${{BASH_VERSINFO[0]}}" -gt 4 ]]; then
61    shopt -u hostcomplete && complete -o nospace -o bashdefault -o nosort -F _{bin_snake} {bin}
62else
63    shopt -u hostcomplete && complete -o nospace -o bashdefault -F _{bin_snake} {bin}
64fi
65# vim: noet ci pi sts=0 sw=4 ts=4 ft=sh
66"#
67    ));
68
69    out.join("\n")
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::test::SPEC_KITCHEN_SINK;
76    use insta::assert_snapshot;
77
78    #[test]
79    fn test_complete_bash() {
80        assert_snapshot!(complete_bash(&CompleteOptions {
81            usage_bin: "usage".to_string(),
82            shell: "bash".to_string(),
83            bin: "mycli".to_string(),
84            cache_key: None,
85            spec: None,
86            usage_cmd: Some("mycli complete --usage".to_string()),
87            include_bash_completion_lib: false,
88        }));
89        assert_snapshot!(complete_bash(&CompleteOptions {
90            usage_bin: "usage".to_string(),
91            shell: "bash".to_string(),
92            bin: "mycli".to_string(),
93            cache_key: Some("1.2.3".to_string()),
94            spec: None,
95            usage_cmd: Some("mycli complete --usage".to_string()),
96            include_bash_completion_lib: false,
97        }));
98        assert_snapshot!(complete_bash(&CompleteOptions {
99            usage_bin: "usage".to_string(),
100            shell: "bash".to_string(),
101            bin: "mycli".to_string(),
102            cache_key: None,
103            spec: Some(SPEC_KITCHEN_SINK.clone()),
104            usage_cmd: None,
105            include_bash_completion_lib: false,
106        }));
107    }
108}