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    let generated_comment = if let Some(source_file) = &opts.source_file {
15        format!("# @generated by usage-cli from {source_file}")
16    } else {
17        "# @generated by usage-cli from usage spec".to_string()
18    };
19    out.push(generated_comment);
20    if opts.include_bash_completion_lib {
21        out.push(include_str!("../../bash-completion/bash_completion").to_string());
22        out.push("\n".to_string());
23    };
24    out.push(format!(
25        r#"_{bin_snake}() {{
26    if ! type -p {usage_bin} &> /dev/null; then
27        echo >&2
28        echo "Error: {usage_bin} CLI not found. This is required for completions to work in {bin}." >&2
29        echo "See https://usage.jdx.dev for more information." >&2
30        return 1
31    fi"#));
32
33    // Build logic to write spec directly to file without storing in shell variables
34    let file_write_logic = if let Some(usage_cmd) = &opts.usage_cmd {
35        if opts.cache_key.is_some() {
36            format!(
37                r#"if [[ ! -f "$spec_file" ]]; then
38        {usage_cmd} > "$spec_file"
39    fi"#
40            )
41        } else {
42            format!(r#"{usage_cmd} > "$spec_file""#)
43        }
44    } else if let Some(spec) = &opts.spec {
45        let heredoc = format!(
46            r#"cat > "$spec_file" <<'__USAGE_EOF__'
47{spec}
48__USAGE_EOF__"#,
49            spec = spec.to_string().trim()
50        );
51        if opts.cache_key.is_some() {
52            format!(
53                r#"if [[ ! -f "$spec_file" ]]; then
54    {heredoc}
55fi"#
56            )
57        } else {
58            heredoc.to_string()
59        }
60    } else {
61        String::new()
62    };
63
64    out.push(format!(
65        r#"
66	local cur prev words cword was_split comp_args
67    _comp_initialize -n : -- "$@" || return
68    local spec_file="${{TMPDIR:-/tmp}}/usage_{spec_variable}.spec"
69    {file_write_logic}
70    # shellcheck disable=SC2207
71	_comp_compgen -- -W "$(command {usage_bin} complete-word --shell bash -f "$spec_file" --cword="$cword" -- "${{words[@]}}")"
72	_comp_ltrim_colon_completions "$cur"
73    # shellcheck disable=SC2181
74    if [[ $? -ne 0 ]]; then
75        unset COMPREPLY
76    fi
77    return 0
78}}
79
80if [[ "${{BASH_VERSINFO[0]}}" -eq 4 && "${{BASH_VERSINFO[1]}}" -ge 4 || "${{BASH_VERSINFO[0]}}" -gt 4 ]]; then
81    shopt -u hostcomplete && complete -o nospace -o bashdefault -o nosort -F _{bin_snake} {bin}
82else
83    shopt -u hostcomplete && complete -o nospace -o bashdefault -F _{bin_snake} {bin}
84fi
85# vim: noet ci pi sts=0 sw=4 ts=4 ft=sh
86"#
87    ));
88
89    out.join("\n")
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::test::SPEC_KITCHEN_SINK;
96    use insta::assert_snapshot;
97
98    #[test]
99    fn test_complete_bash() {
100        assert_snapshot!(complete_bash(&CompleteOptions {
101            usage_bin: "usage".to_string(),
102            shell: "bash".to_string(),
103            bin: "mycli".to_string(),
104            cache_key: None,
105            spec: None,
106            usage_cmd: Some("mycli complete --usage".to_string()),
107            include_bash_completion_lib: false,
108            source_file: None,
109        }));
110        assert_snapshot!(complete_bash(&CompleteOptions {
111            usage_bin: "usage".to_string(),
112            shell: "bash".to_string(),
113            bin: "mycli".to_string(),
114            cache_key: Some("1.2.3".to_string()),
115            spec: None,
116            usage_cmd: Some("mycli complete --usage".to_string()),
117            include_bash_completion_lib: false,
118            source_file: None,
119        }));
120        assert_snapshot!(complete_bash(&CompleteOptions {
121            usage_bin: "usage".to_string(),
122            shell: "bash".to_string(),
123            bin: "mycli".to_string(),
124            cache_key: None,
125            spec: Some(SPEC_KITCHEN_SINK.clone()),
126            usage_cmd: None,
127            include_bash_completion_lib: false,
128            source_file: None,
129        }));
130    }
131}