]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
Add bash completion
authorDavid Woodhouse <dwmw2@infradead.org>
Tue, 7 Apr 2020 13:12:27 +0000 (14:12 +0100)
committerDavid Woodhouse <dwmw2@infradead.org>
Tue, 7 Apr 2020 15:23:29 +0000 (16:23 +0100)
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Makefile.am
bash/openconnect.bash [new file with mode: 0644]
main.c
openconnect.spec.in
www/changelog.xml

index e8ec3825e1070f34eab0a0b48c83a1c231cc6f5b..ac155aab2857126a71aa195dafb48df9eb62dc17 100644 (file)
@@ -135,13 +135,16 @@ pkgconfig_DATA = openconnect.pc
 
 EXTRA_DIST = AUTHORS version.sh COPYING.LGPL $(lib_srcs_openssl) $(lib_srcs_gnutls)
 EXTRA_DIST += $(shell cd "$(top_srcdir)" && \
-               git ls-tree HEAD -r --name-only -- android/ java/ trojans/ 2>/dev/null)
+               git ls-tree HEAD -r --name-only -- android/ java/ trojans/ bash/ 2>/dev/null)
 
 DISTCLEANFILES = $(pkgconfig_DATA)
 
 pkglibexec_SCRIPTS = trojans/csd-post.sh trojans/csd-wrapper.sh trojans/tncc-wrapper.py \
        trojans/hipreport.sh trojans/hipreport-android.sh
 
+bashcompletiondir = $(sysconfdir)/bash_completion.d
+bashcompletion_DATA = bash/openconnect.bash
+
 # main.c includes version.c
 openconnect-main.$(OBJEXT): version.c
 
diff --git a/bash/openconnect.bash b/bash/openconnect.bash
new file mode 100644 (file)
index 0000000..89d55ee
--- /dev/null
@@ -0,0 +1,107 @@
+#
+# Bash completion for OpenConnect
+#
+# Copyright © David Woodhouse <dwmw2@infradead.org>
+#
+# Author: David Woodhouse <dwmw2@infradead.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# version 2.1, as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+
+
+# Consider a command line like the following:
+#
+# openconnect -c --authenticate\ -k -k "'"'"'.pem --authgroup 'foo
+# bar' --o\s linux-64 myserver
+#
+# There is absolutely no way I want to attempt parsing that in C and
+# attempting to come up with the correct results as bash would do.
+# That is just designing for failure; we'll never get it right.
+#
+# Yet if we use 'complete -C openconnect openconnect' and allow the
+# program to do completions all by itself, that's what bash expects
+# it to do. All that's passed into the program is $COMP_LINE and
+# some other metadata.
+#
+# So instead we use bash to help us. In a completion *function* we
+# are given the ${COMP_WORDS[]} array which has actually been parsed
+# correctly. We still want openconnect itself to be able to do the
+# result generation, so just prepend --autocomplete to the args.
+#
+# For special cases like filenames and hostnames, we want to invoke
+# compgen, again to avoid reinventing the wheel badly. So define
+# special cases HOSTNAME, FILENAME as the autocomplete results,
+# to be handled as special cases. In those cases we also use
+# ${COMP_WORDS[$COMP_CWORD]}) as the string to bew completed,
+# pristine from bash instead of having been passed through the
+# program itself. Thus, we see correct completions along the lines
+# of
+#
+#     $ ls foo\ *
+#     'foo bar.pem'  'foo bar.xml'  'foo baz.crt'
+#     $ openconnect -c ./fo<TAB>
+#
+# ... partially completes to:
+#
+#     $ openconnect -c ./foo\ ba
+#
+# ... and a second <TAB> shows:
+#
+#     foo bar.pem  foo baz.crt
+#
+# Likewise,
+#
+#     $ touch '"'"'".pem
+#     $ openconnect -c '"'<TAB>
+#
+# ...completes to:
+#
+#    $ openconnect -c \"\'.pem
+#
+# This does fall down if I create a filename with a newline in it,
+# but even tab-completion for 'ls' falls over in that case.
+#
+# The main problem with this approach is that we can't easily map
+# $COMP_POINT to the precise character on the line at which TAB was
+# being pressed, which may not be the *end*.
+
+
+_complete_openconnect () {
+    export COMP_LINE COMP_POINT COMP_CWORD COMP_KEY COMP_TYPE
+    COMP_WORDS[0]="--autocomplete"
+    local IFS=$'\n'
+    COMPREPLY=( $(/home/dwmw/git/openconnect/gtls-ibm/openconnect "${COMP_WORDS[@]}") )
+    case "${COMPREPLY[0]}" in
+       FILENAME)
+           if [ "${COMPREPLY[1]}" != "" ]; then
+               COMPREPLY=( $( compgen -f -o filenames -o plusdirs -X ${COMPREPLY[1]} ${COMP_WORDS[$COMP_CWORD]}) )
+           else
+               COMPREPLY=( $( compgen -f -o filenames -o plusdirs ${COMP_WORDS[$COMP_CWORD]}) )
+           fi
+           ;;
+
+       FILENAMEAT)
+           COMPREPLY=( $( compgen -P @ -f -o filenames -o plusdirs ${COMP_WORDS[$COMP_CWORD]#@}) )
+           ;;
+
+       EXECUTABLE)
+           COMPREPLY=( $( compgen -c -o plusdirs ${COMP_WORDS[$COMP_CWORD]}) )
+           ;;
+
+       HOSTNAME)
+           COMPREPLY=( $( compgen -A hostname ${COMP_WORDS[$COMP_CWORD]}) )
+           ;;
+
+       USERNAME)
+           COMPREPLY=( $( compgen -A user ${COMP_WORDS[$COMP_CWORD]}) )
+           ;;
+    esac
+}
+
+complete -F _complete_openconnect -o filenames openconnect
diff --git a/main.c b/main.c
index 32823586311a77496b6e84bfbe681a62daeaf2aa..366c5770807772548a6f5f46a8b43f50be735456 100644 (file)
--- a/main.c
+++ b/main.c
@@ -1100,6 +1100,218 @@ static void get_uids(const char *config_arg, uid_t *uid, gid_t *gid)
 }
 #endif
 
+static int complete_words(char *partial, ...)
+{
+       int partlen = strlen(partial);
+       va_list vl;
+       char *check;
+
+       va_start(vl, partial);
+       while ( (check = va_arg(vl, char *)) ) {
+               if (!strncmp(partial, check, partlen))
+                       printf("%s\n", check);
+       }
+       va_end(vl);
+       return 0;
+}
+
+static int autocomplete(int argc, char **argv)
+{
+       int opt;
+       const char *comp_cword = getenv("COMP_CWORD");
+       char *comp_opt;
+       int cword, longidx;
+
+       /* Skip over the --autocomplete */
+       argc--;
+       argv++;
+
+       if (!comp_cword)
+               return -EINVAL;
+
+       cword = atoi(comp_cword);
+       if (cword <= 0 || cword > argc)
+               return -EINVAL;
+
+       comp_opt = argv[cword];
+       if (!comp_opt)
+               return -EINVAL;
+
+       opterr = 0;
+
+       while (1) {
+               int match_opt = (argv[optind] == comp_opt);
+
+               /* Don't let getopt_long() assume it's a separator; instead
+                * assume they want to tab-complete to a real long option. */
+               if (match_opt && !strcmp(comp_opt, "--"))
+                       goto empty_opt;
+
+               opt = getopt_long(argc, argv,
+#ifdef _WIN32
+                                 "C:c:Dde:F:g:hi:k:m:P:p:Q:qs:u:Vvx:",
+#else
+                                 "bC:c:Dde:F:g:hi:k:lm:P:p:Q:qSs:U:u:Vvx:",
+#endif
+                                 long_options, &longidx);
+
+               if (opt == -1)
+                       break;
+
+               if (match_opt) {
+               empty_opt:
+                       /* No autocompletion for short options */
+                       if (!strncmp(comp_opt, "--", 2)) {
+                               int complen = strlen(comp_opt + 2);
+                               const struct option *p = long_options;
+
+                               while (p->name) {
+                                       if (!strncmp(comp_opt + 2, p->name, complen))
+                                               printf("--%s\n", p->name);
+                                       p++;
+                               }
+                       }
+                       return 0;
+               }
+
+
+               if (optarg == comp_opt) {
+                       switch (opt) {
+                       case 'k': /* --sslkey */
+                       case 'c': /* --certificate */
+                               if (!strncmp(comp_opt, "pkcs11:", 7)) {
+                                       /* We could do clever things here... */
+                                       return 0; /* .. but we don't. */
+                               }
+                               printf("FILENAME\n!*.@(pem|der|p12|crt)\n");
+                               break;
+
+                       case OPT_CAFILE: /* --cafile */
+                               printf("FILENAME\n!*.@(pem|der|crt)\n");
+                               break;
+
+                       case 'x': /* --xmlconfig */
+                               printf("FILENAME\n!*.xml\n");
+                               break;
+
+                       case OPT_CONFIGFILE: /* --config */
+                       case OPT_PIDFILE: /* --pid-file */
+                               printf("FILENAME\n");
+                               break;
+
+                       case 's': /* --script */
+                       case OPT_CSD_WRAPPER: /* --csd-wrapper */
+                               printf("EXECUTABLE\n");
+                               break;
+
+                       case OPT_LOCAL_HOSTNAME: /* --local-hostname */
+                               printf("HOSTNAME\n");
+                               break;
+
+                       case OPT_CSD_USER: /* --csd-user */
+                       case 'U': /* --setuid */
+                               printf("USERNAME\n");
+                               break;
+
+                       case OPT_OS: /* --os */
+                               complete_words(comp_opt, "mac-intel", "android",
+                                              "linux-64", "linux", "apple-ios",
+                                              "win", NULL);
+                               break;
+
+                       case OPT_COMPRESSION: /* --compression */
+                               complete_words(comp_opt, "none", "off", "all",
+                                              "stateless", NULL);
+                               break;
+
+                       case OPT_PROTOCOL: /* --protocol */
+                       {
+                               struct oc_vpn_proto *protos, *p;
+                               int partlen = strlen(comp_opt);
+
+                               if (openconnect_get_supported_protocols(&protos) >= 0) {
+                                       for (p = protos; p->name; p++) {
+                                               if(!strncmp(comp_opt, p->name, partlen))
+                                                       printf("%s\n", p->name);
+                                       }
+                                       free(protos);
+                               }
+                               break;
+                       }
+
+                       case OPT_HTTP_AUTH: /* --http-auth */
+                       case OPT_PROXY_AUTH: /* --proxy-auth */
+                               /* FIXME: Expand latest list item */
+                               break;
+
+                       case OPT_TOKEN_MODE: /* --token-mode */
+                               complete_words(comp_opt, "totp", "hotp", "oidc", NULL);
+                               if (openconnect_has_stoken_support())
+                                       complete_words(comp_opt, "rsa", NULL);
+                               if (openconnect_has_yubioath_support())
+                                       complete_words(comp_opt, "yubioath", NULL);
+                               break;
+
+                       case OPT_TOKEN_SECRET: /* --token-secret */
+                               if (!comp_opt[0] || comp_opt[0] == '/')
+                                       printf("FILENAME\n");
+                               else if (comp_opt[0] == '@')
+                                       printf("FILENAMEAT\n");
+                               break;
+
+                       case 'i': /* --interface */
+                               /* FIXME: Enumerate available tun devices */
+                               break;
+
+                       case OPT_SERVERCERT: /* --servercert */
+                               /* We could do something really evil here and actually
+                                * connect, then return the result? */
+                               break;
+
+                       /* No autocmplete for these but handle them explicitly so that
+                        * we can have automatic checking for *accidentally* unhandled
+                        * options. Right after we do automated checking of man page
+                        * entries and --help output for all supported options too. */
+
+                       case 'e': /* --cert-expire-warning */
+                       case 'C': /* --cookie */
+                       case 'g': /* --usergroup */
+                       case 'm': /* --mtu */
+                       case OPT_BASEMTU: /* --base-mtu */
+                       case 'p': /* --key-password */
+                       case 'P': /* --proxy */
+                       case 'u': /* --user */
+                       case 'Q': /* --queue-len */
+                       case OPT_RECONNECT_TIMEOUT: /* --reconnect-timeout */
+                       case OPT_AUTHGROUP: /* --authgroup */
+                       case OPT_RESOLVE: /* --resolve */
+                       case OPT_USERAGENT: /* --useragent */
+                       case OPT_VERSION: /* --version-string */
+                       case OPT_FORCE_DPD: /* --force-dpd */
+                       case OPT_FORCE_TROJAN: /* --force-trojan */
+                       case OPT_DTLS_LOCAL_PORT: /* --dtls-local-port */
+                       case 'F': /* --form-entry */
+                       case OPT_GNUTLS_DEBUG: /* --gnutls-debug */
+                       case OPT_CIPHERSUITES: /* --gnutls-priority */
+                       case OPT_DTLS_CIPHERS: /* --dtls-ciphers */
+                       case OPT_DTLS12_CIPHERS: /* --dtls12-ciphers */
+                               break;
+
+                       default:
+                               fprintf(stderr, _("Unhandled autocomplete for option %d '--%s'. Please report.\n"),
+                                       opt, long_options[longidx].name);
+                               return -ENOENT;
+                       }
+
+                       return 0;
+               }
+       }
+
+       /* Ths only non-option argument we accept as a hostname */
+       printf("HOSTNAME\n");
+       return 0;
+}
+
 int main(int argc, char **argv)
 {
        struct openconnect_info *vpninfo;
@@ -1137,6 +1349,9 @@ int main(int argc, char **argv)
                fprintf(stderr,
                        _("WARNING: Cannot set locale: %s\n"), strerror(errno));
 
+       if (argc > 2 && !strcmp(argv[1], "--autocomplete"))
+               return autocomplete(argc, argv);
+
 #ifdef HAVE_NL_LANGINFO
        charset = nl_langinfo(CODESET);
        if (charset && strcmp(charset, "UTF-8"))
index 03be1ea6eeee965dc3ac993e4163905801bf54d2..f5a0ba8b37569424f9abd7515cea6ddc092ec5f0 100644 (file)
@@ -145,6 +145,7 @@ make VERBOSE=1 XFAIL_TESTS="auth-nonascii bad_dtls_test" check
 %{_sbindir}/openconnect
 %{_libexecdir}/openconnect/
 %{_mandir}/man8/*
+%{_sysconfdir}/bash_completion.d
 %doc TODO COPYING.LGPL
 %doc %{_pkgdocdir}
 
index c44324d314b75ed6017f0bed6f5d44aa446a3d1a..fb1b9a17aa3abd55c1e2270ed4fd4c17200f14d9 100644 (file)
@@ -15,7 +15,7 @@
 <ul>
    <li><b>OpenConnect HEAD</b>
      <ul>
-       <li><i>No changelog entries yet</i></li>
+       <li>Add bash completion support.</li>
      </ul><br/>
   </li>
   <li><b><a href="ftp://ftp.infradead.org/pub/openconnect/openconnect-8.08.tar.gz">OpenConnect v8.08</a></b>