From 638cbbe332c274d14b87facbb620c6e3acfc41f5 Mon Sep 17 00:00:00 2001 From: Szabina Korbai Date: Wed, 10 Sep 2025 09:08:31 +0100 Subject: [PATCH] libutil: Implement zsh and bash autocompletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add functionality for generating autocompletion scripts, allowing for tab completion of tool options for bash and zsh. This functionality relies on reading the available options from the util_opt struct at runtime. The script generation happens on the build system. Reviewed-by: Jan Höppner Reviewed-by: Steffen Eiden Signed-off-by: Szabina Korbai Signed-off-by: Steffen Eiden --- common_autocomp.mak | 81 ++++++++++ include/lib/util_autocomp.h | 22 +++ libutil/Makefile | 7 +- libutil/util_autocomp_host.c | 299 +++++++++++++++++++++++++++++++++++ 4 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 common_autocomp.mak create mode 100644 include/lib/util_autocomp.h create mode 100644 libutil/util_autocomp_host.c diff --git a/common_autocomp.mak b/common_autocomp.mak new file mode 100644 index 00000000..2cac7c6d --- /dev/null +++ b/common_autocomp.mak @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: MIT +# Copyright IBM Corp. 2025 + + +# This file defines the build process for shell autocompletion binaries +# +# How to incorporate it into tool Makefiles: +# +# 0. The file with the autocompletion script generation code should be named +# 'autocompletion_generator_host.c' +# +# 1. Define the 'bash-completions' and 'zsh-completions' variables: both must be a list +# of the names of all the shell completion scripts that belong to the tools the Makefile +# is responsible for building. +# +# (The name of a zsh completion script is the same as the name of the tool, +# prefixed by an underscore) +# +# (The name of a bash completion script is the same as the name of the tool, +# suffixed by '.bash') +# +# 2. include this file +# +# +1: Add the autocompletion scripts to the .gitignore file +# +# (See cpumf or dasdfmt as example) + + +autocomplete-bin := autocompletion_generator_host + +autocomp-object := $(rootdir)/libutil/util_autocomp_host.o + +$(autocomp-object): $(rootdir)/libutil + $(MAKE) -C $(rootdir)/libutil/ $(notdir $@) + +$(autocomplete-bin).o: $(autocomplete-bin).c + $(CC_FOR_BUILD) $(CFLAGS_FOR_BUILD) -I $(rootdir)/include -c $< -o $@ + +$(autocomplete-bin): $(autocomplete-bin).o $(autocomp-object) + $(LINK_FOR_BUILD) $^ -o $@ + +$(bash-completions) $(zsh-completions) &: $(autocomplete-bin) + ./$(autocomplete-bin) + +install-shell-completions: $(bash-completions) $(zsh-completions) + $(INSTALL) -d -m 755 $(DESTDIR)$(ZSHCOMPLETIONDIR) + $(INSTALL) -d -m 755 $(DESTDIR)$(BASHCOMPLETIONDIR) + for completion in $(bash-completions); do \ + $(INSTALL) -m 644 $$completion $(DESTDIR)$(BASHCOMPLETIONDIR); \ + done + for completion in $(zsh-completions); do \ + $(INSTALL) -m 644 $$completion $(DESTDIR)$(ZSHCOMPLETIONDIR); \ + done + +all: $(zsh-completions) $(bash-completions) + +install: install-shell-completions + +.PHONY: install-shell-completions $(rootdir)/libutil + +clean-autocomplete-bin: + $(RM) -- $(autocomplete-bin) + +clean: clean-autocomplete-bin + +ifdef bash-completions +clean-bash-completions: + $(RM) -- $(bash-completions) + +clean: clean-bash-completions +endif + + +ifdef zsh-completions +clean-zsh-completions: + $(RM) -- $(zsh-completions) + +clean: clean-zsh-completions +endif + +.PHONY: clean-autocomplete-bin clean-bash-completions clean-zsh-completions diff --git a/include/lib/util_autocomp.h b/include/lib/util_autocomp.h new file mode 100644 index 00000000..551600ae --- /dev/null +++ b/include/lib/util_autocomp.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: MIT */ +/* + * autocomp - command line autocompletion + * + * Generating autocompletion scripts for bash and zsh + * based on util_opt struct + * + * Copyright IBM Corp. 2025 + * + * s390-tools is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + * + */ + +#ifndef LIB_UTIL_AUTOCOMP_H +#define LIB_UTIL_AUTOCOMP_H + +#include "lib/util_opt.h" + +void generate_autocomp(struct util_opt *opt_vec, char *tool_name); + +#endif diff --git a/libutil/Makefile b/libutil/Makefile index 508df5ba..053ebfe9 100644 --- a/libutil/Makefile +++ b/libutil/Makefile @@ -1,13 +1,16 @@ include ../common.mak +include ../common_autocomp.mak lib := libutil.a -sources := $(filter-out %_example.c,$(wildcard *.c)) +sources := $(filter-out %_example.c %_host.c, $(wildcard *.c)) objects := $(patsubst %.c,%.o,$(sources)) +autocomp-object := util_autocomp_host.o + examples := $(patsubst %.c,%,$(wildcard *_example.c)) -all: $(lib) +all: $(lib) $(autocomp-object) examples: $(examples) $(examples): %: %.o $(lib) diff --git a/libutil/util_autocomp_host.c b/libutil/util_autocomp_host.c new file mode 100644 index 00000000..3d6bcb39 --- /dev/null +++ b/libutil/util_autocomp_host.c @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: MIT +/* + * autocomp - command line autocompletion + * + * Generating autocompletion scripts for bash and zsh + * based on util_opt struct + * + * Copyright IBM Corp. 2025 + * + * s390-tools is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + * + */ + +#include +#include +#include +#include +#include +#include + +#include "lib/util_autocomp.h" +#include "lib/util_opt.h" + +static const char *bash_script_part1 = "() {\n\n\ +\tlocal current_word previous_word options_array\n\n\ +\tCOMPREPLY=()\n\n\ +\tcurrent_word=\"${COMP_WORDS[COMP_CWORD]}\"\n\n\ +\tprevious_word=\"${COMP_WORDS[COMP_CWORD-1]}\"\n\n\ +\toptions_array=\""; + +static const char *bash_script_part2 = "\tif [[ ${current_word} == -* || ${COMP_CWORD} -eq 1 ]] ; then\n\n\ +\t\tCOMPREPLY=( $(compgen -W \"${options_array}\" -- ${current_word} ) )\n\n\ +\t\treturn 0\n\n\ +\tfi\n\n\ +}\n\n\ +complete -F "; + +static char *format_name(const char *fmt, char *tool_name) +{ + char *func_name; + + if (asprintf(&func_name, fmt, tool_name) == -1) + return NULL; + return func_name; +} + +/* + * The convention for a completion function name is to be the same + * as the command's name, but prefixed by '_'. + */ +static char *generate_func_name(char *tool_name) +{ + return format_name("_%s", tool_name); +} + +static char *generate_bash_filename(char *tool_name) +{ + return format_name("%s.bash", tool_name); +} + +static int init_scriptfile(char *file_path) +{ + int fd; + + fd = open(file_path, O_CREAT | O_WRONLY, 0644); + if (fd < 0) + return -EIO; + return fd; +} + +static int start_bash_scriptfile(int fd, char *func_name) +{ + int len, ret = 0; + char *str; + + len = asprintf(&str, "%s%s", func_name, bash_script_part1); + if (len == -1) + return -EIO; + + if (write(fd, str, len) != len) + ret = -EIO; + free(str); + return ret; +} + +static int start_zsh_scriptfile(int fd, char *func_name, char *tool_name) +{ + const char *part3 = " {\n\n\t_arguments -C \\\n"; + const char *part2 = "\n\nfunction "; + const char *part1 = "#compdef "; + int len, ret = 0; + char *str; + + len = asprintf(&str, "%s%s%s%s%s", part1, tool_name, part2, func_name, part3); + if (len == -1) + return -EIO; + + if (write(fd, str, len) != len) + ret = -EIO; + free(str); + return ret; +} + +static int write_bash_command_options(struct util_opt *opt_vec, int fd) +{ + const char *prefix = " --"; + char *str; + int len; + + for (int i = 0; opt_vec[i].desc; i++) { + if (opt_vec[i].option.name) { + len = asprintf(&str, "%s%s", prefix, opt_vec[i].option.name); + if (len == -1) + return -EIO; + if (write(fd, str, len) != len) { + free(str); + return -EIO; + } + free(str); + } + } + if (write(fd, "\"\n\n", 3) != 3) + return -EIO; + return 0; +} + +static int write_zsh_command_options(struct util_opt *opt_vec, int fd) +{ + const char *name, *desc; + char *str; + int len; + + for (int i = 0; opt_vec[i].desc; i++) { + if (opt_vec[i].option.name) { + name = opt_vec[i].option.name; + desc = opt_vec[i].desc; + len = asprintf(&str, "\t\t\"--%s[%s]\" \\\n", name, desc); + if (len == -1) + return -EIO; + if (write(fd, str, len) != len) { + free(str); + return -EIO; + } + free(str); + } + } + if (write(fd, "\n}\n", 3) != 3) + return -EIO; + return 0; +} + +static int finish_bash_scriptfile(char *tool_name, int fd, char *func_name) +{ + int len, ret = 0; + char *str; + + len = asprintf(&str, "%s%s %s\n", bash_script_part2, func_name, tool_name); + if (len == -1) + return -EIO; + + if (write(fd, str, len) != len) + ret = -EIO; + free(str); + return ret; +} + +/* + * Adds tab completion in bash for a command. + * Works by generating an autocompletion + * script file at '/usr/share/bash-completion/completions'. + * + * The full script will be as follows, supposing the tool name is + * 'example' and it only has the options '--help' and + * '--version': + * + * _example() { + * + * local current_word previous_word options_array + * + * COMPREPLY=() + * + * current_word="${COMP_WORDS[COMP_CWORD]}" + * + * previous_word="${COMP_WORDS[COMP_CWORD-1]}" + * + * options_array="--version --help" + * + * if [[ ${current_word} == -* || ${COMP_CWORD} -eq 1 ]] ; then + * + * COMPREPLY=( $(compgen -W "${options_array}" -- ${current_word} ) ) + * + * return 0 + * + * fi + * + * } + * + * complete -F _example example + * + */ +static void generate_bash_autocomp(struct util_opt *opt_vec, char *tool_name) +{ + char *func_name, *filename; + int fd, ret = 0; + + func_name = generate_func_name(tool_name); + if (!func_name) { + ret = -ENOMEM; + goto end; + } + filename = generate_bash_filename(tool_name); + if (!filename) { + ret = -ENOMEM; + goto free_func; + } + fd = init_scriptfile(filename); + if (fd < 0) { + ret = fd; + goto free_file; + } + ret = start_bash_scriptfile(fd, func_name); + if (ret < 0) + goto close; + ret = write_bash_command_options(opt_vec, fd); + if (ret < 0) + goto close; + ret = finish_bash_scriptfile(tool_name, fd, func_name); +close: + close(fd); + if (ret) + remove(filename); +free_file: + free(filename); +free_func: + free(func_name); +end: + printf(" AUTOCOMP\t%s/%s.bash\n", tool_name, tool_name); + if (ret) + printf("%s.bash: error - %s\n", tool_name, strerror(abs(ret))); +} + +/* + * Adds tab completion in zsh for a command. + * Works by generating an autocompletion + * script file at '/usr/share/zsh/site-functions'. + * + * The full script will be as follows, supposing the tool name is + * 'example' and it only has the options '--help', -h and + * '--version' (the descriptions, as well as the flags are + * taken from a util_opt struct): + * + * #compdef example_completion + * + * function _example_completion { + * + * _arguments -C \ + * "-h[Show help information]" \ + * "--help[Show help but long format]" \ + * "--version[Show version]" + * } + * + */ +static void generate_zsh_autocomp(struct util_opt *opt_vec, char *tool_name) +{ + char *func_name; + int fd, ret = 0; + + func_name = generate_func_name(tool_name); + if (!func_name) { + ret = -ENOMEM; + goto end; + } + fd = init_scriptfile(func_name); + if (fd < 0) { + ret = fd; + goto free_func; + } + ret = start_zsh_scriptfile(fd, func_name, tool_name); + if (ret < 0) + goto close; + ret = write_zsh_command_options(opt_vec, fd); +close: + close(fd); + if (ret) + remove(func_name); +free_func: + free(func_name); +end: + printf(" AUTOCOMP\t%s/_%s\n", tool_name, tool_name); + if (ret) + printf("_%s: error - %s\n", tool_name, strerror(abs(ret))); +} + +void generate_autocomp(struct util_opt *opt_vec, char *tool_name) +{ + generate_bash_autocomp(opt_vec, tool_name); + generate_zsh_autocomp(opt_vec, tool_name); +}