From 93dc557c56269243b7bd3c7d9f75880fb74a17a8 Mon Sep 17 00:00:00 2001 From: Johannes Lorenz <1042576+JohannesLorenz@users.noreply.github.com> Date: Mon, 24 Sep 2018 03:17:39 +0200 Subject: [PATCH] Add bash completion (#4534) (#4604) * Add bash completion (#4534) --- cmake/modules/BashCompletion.cmake | 92 ++++++++ doc/CMakeLists.txt | 3 + doc/bash-completion/CMakeLists.txt | 4 + doc/bash-completion/lmms | 334 +++++++++++++++++++++++++++++ 4 files changed, 433 insertions(+) create mode 100644 cmake/modules/BashCompletion.cmake create mode 100644 doc/bash-completion/CMakeLists.txt create mode 100644 doc/bash-completion/lmms diff --git a/cmake/modules/BashCompletion.cmake b/cmake/modules/BashCompletion.cmake new file mode 100644 index 000000000..0dc016178 --- /dev/null +++ b/cmake/modules/BashCompletion.cmake @@ -0,0 +1,92 @@ +# A wrapper around pkg-config-provided and cmake-provided bash completion that +# will have dynamic behavior at INSTALL() time to allow both root-level +# INSTALL() as well as user-level INSTALL(). +# +# See also https://github.com/scop/bash-completion +# +# Copyright (c) 2018, Tres Finocchiaro, +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. +# +# Usage: +# INCLUDE(BashCompletion) +# BASHCOMP_INSTALL(foo) +# ... where "foo" is a shell script adjacent to the CMakeLists.txt +# +# How it determines BASHCOMP_PKG_PATH, in order: +# 1. Uses BASHCOMP_PKG_PATH if already set (e.g. -DBASHCOMP_PKG_PATH=...) +# a. If not, uses pkg-config's PKG_CHECK_MODULES to determine path +# b. Fallback to cmake's FIND_PACKAGE(bash-completion) path +# c. Fallback to hard-coded /usr/share/bash-completion/completions +# 2. Final fallback to ${CMAKE_INSTALL_PREFIX}/share/bash-completion/completions if +# detected path is unwritable. + +# - Windows does not support bash completion +# - macOS support should eventually be added for Homebrew (TODO) +IF(WIN32) + MESSAGE(STATUS "Bash competion is not supported on this platform.") +ELSEIF(APPLE) + MESSAGE(STATUS "Bash completion is not yet implemented for this platform.") +ELSE() + INCLUDE(FindUnixCommands) + # Honor manual override if provided + IF(NOT BASHCOMP_PKG_PATH) + # First, use pkg-config, which is the most reliable + FIND_PACKAGE(PkgConfig QUIET) + IF(PKGCONFIG_FOUND) + PKG_CHECK_MODULES(BASH_COMPLETION bash-completion) + PKG_GET_VARIABLE(BASHCOMP_PKG_PATH bash-completion completionsdir) + ELSE() + # Second, use cmake (preferred but less common) + FIND_PACKAGE(bash-completion QUIET) + IF(BASH_COMPLETION_FOUND) + SET(BASHCOMP_PKG_PATH "${BASH_COMPLETION_COMPLETIONSDIR}") + ENDIF() + ENDIF() + + # Third, use a hard-coded fallback value + IF("${BASHCOMP_PKG_PATH}" STREQUAL "") + SET(BASHCOMP_PKG_PATH "/usr/share/bash-completion/completions") + ENDIF() + ENDIF() + + # Always provide a fallback for non-root INSTALL() + SET(BASHCOMP_USER_PATH "${CMAKE_INSTALL_PREFIX}/share/bash-completion/completions") + + # Cmake doesn't allow easy use of conditional logic at INSTALL() time + # this is a problem because ${BASHCOMP_PKG_PATH} may not be writable and we + # need sane fallback behavior for bundled INSTALL() (e.g. .AppImage, etc). + # + # The reason this can't be detected by cmake is that it's fairly common to + # run "cmake" as a one user (i.e. non-root) and "make install" as another user + # (i.e. root). + # + # - Creates a script called "install_${SCRIPT_NAME}_completion.sh" into the + # working binary directory and invokes this script at install. + # - Script handles INSTALL()-time conditional logic for sane ballback behavior + # when ${BASHCOMP_PKG_PATH} is unwritable (i.e. non-root); Something cmake + # can't handle on its own at INSTALL() time) + MACRO(BASHCOMP_INSTALL SCRIPT_NAME) + # A shell script for wrapping conditionl logic + SET(BASHCOMP_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_${SCRIPT_NAME}_completion.sh") + + FILE(WRITE ${BASHCOMP_SCRIPT} "\ +#!${BASH}\n\ +set -e\n\ +BASHCOMP_PKG_PATH=\"${BASHCOMP_USER_PATH}\"\n\ +if [ -w \"${BASHCOMP_PKG_PATH}\" ]; then\n\ + BASHCOMP_PKG_PATH=\"${BASHCOMP_PKG_PATH}\"\n\ +fi\n\ +echo -e \"\\nInstalling bash completion...\\n\"\n\ +mkdir -p \"\$BASHCOMP_PKG_PATH\"\n\ +cp \"${CMAKE_CURRENT_SOURCE_DIR}/${SCRIPT_NAME}\" \"\$BASHCOMP_PKG_PATH\"\n\ +chmod a+r \"\$BASHCOMP_PKG_PATH/${SCRIPT_NAME}\"\n\ +echo -e \"Bash completion for ${SCRIPT_NAME} has been installed to \$BASHCOMP_PKG_PATH/${SCRIPT_NAME}\"\n\ +") + INSTALL(CODE "EXECUTE_PROCESS(COMMAND chmod u+x \"install_${SCRIPT_NAME}_completion.sh\" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} )") + INSTALL(CODE "EXECUTE_PROCESS(COMMAND \"./install_${SCRIPT_NAME}_completion.sh\" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} )") + + MESSAGE(STATUS "Bash completion script for ${SCRIPT_NAME} will be installed to ${BASHCOMP_PKG_PATH} or fallback to ${BASHCOMP_USER_PATH} if unwritable.") + ENDMACRO() +ENDIF() + diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 7aca74d87..859c5f2c6 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -15,3 +15,6 @@ if(DOXYGEN_FOUND) COMMENT "Generating API documentation with Doxygen" SOURCES Doxyfile.in) endif(DOXYGEN_FOUND) + +ADD_SUBDIRECTORY(bash-completion) + diff --git a/doc/bash-completion/CMakeLists.txt b/doc/bash-completion/CMakeLists.txt new file mode 100644 index 000000000..783e13a71 --- /dev/null +++ b/doc/bash-completion/CMakeLists.txt @@ -0,0 +1,4 @@ +INCLUDE(BashCompletion) +IF(COMMAND BASHCOMP_INSTALL) + BASHCOMP_INSTALL(lmms) +ENDIF() diff --git a/doc/bash-completion/lmms b/doc/bash-completion/lmms new file mode 100644 index 000000000..19fbf4723 --- /dev/null +++ b/doc/bash-completion/lmms @@ -0,0 +1,334 @@ +# lmms(1) completion -*- shell-script -*- +# use shellcheck: "shellcheck -e bash " + +_lmms_array_contains () +{ + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +_lmms_long_param_of() +{ + case "$1" in + -a) + echo "float" + ;; + -b) + echo "bitrate" + ;; + -c) + echo "config" + ;; + -f) + echo "format" + ;; + -i) + echo "interpolation" + ;; + -l) + echo "loop" + ;; + -m) + echo "mode" + ;; + -o) + echo "output" + ;; + -p) + echo "profile" + ;; + -s) + echo "samplerate" + ;; + -x) + echo "oversampling" + ;; + *) + echo "" + ;; + esac +} + +_lmms_conv_old_action () +{ + case "$1" in + -d|--dump) + echo "dump" + ;; + -r|--render) + echo "render" + ;; + --rendertracks) + echo "rendertracks" + ;; + -u|--upgrade) + echo "upgrade" + ;; + *) + echo "" + ;; + esac +} + +_lmms() +{ + local cword=$COMP_CWORD + local cur="${COMP_WORDS[COMP_CWORD]}" + + # call routine provided by bash-completion + _init_completion || return + + local params filemode filetypes + local i # counter variable + local pars_global pars_noaction pars_render actions shortargs + pars_global=(--allowroot --config --help --version) + pars_noaction=(--geometry --import) + pars_render=(--float --bitrate --format --interpolation) + pars_render+=(--loop --mode --output --profile) + pars_render+=(--samplerate --oversampling) + actions=(dump render rendertracks upgrade) + actions_old=(-d --dump -r --render --rendertracks -u --upgrade) + shortargs+=(-a -b -c -f -h -i -l -m -o -p -s -v -x) + + local prev prev2 + if [ "$cword" -gt 1 ] + then + prev=${COMP_WORDS[cword-1]} + fi + if [ "$cword" -gt 2 ] + then + prev2=${COMP_WORDS[cword-2]} + fi + + # don't show shortargs, but complete them when entered + if [[ $cur =~ ^-[^-]$ ]] + then + if _lmms_array_contains "$cur" "${shortargs[@]}" + then + COMPREPLY=( "$cur" ) + fi + return + fi + + # + # please keep those in order like def_pars_args above + # + case $prev in + --bitrate|-b) + params="64 96 128 160 192 256 320" + ;; + --config|-c) + filetypes='xml' + filemode='existing_files' + ;; + --format|-f) + params='wav ogg mp3' + ;; + --geometry) + # we can not name all possibilities, but this helps the user + # by showing them how the format is + params='0x0+0+0' + ;; + --interpolation|-i) + params='linear sincfastest sincmedium sincbest' + ;; + --import) + filetypes='mid|midi|MID|MIDI|rmi|RMI|h2song|H2SONG' + filemode='existing_files' + ;; + --mode|-m) + params='s j m' + ;; + --output|-o) + # default assumption: could be both + local render=1 rendertracks=1 + for i in "${!COMP_WORDS[@]}" + do + if [[ ${COMP_WORDS[i]} =~ ^(render|-r|--render)$ ]] + then + rendertracks= + elif [[ ${COMP_WORDS[i]} =~ ^(rendertracks|--rendertracks)$ ]] + then + render= + fi + done + if [ "$rendertracks" ] + then + filemode='existing_directories' + fi + if [ "$render" ] + then + # filemode files is a superset of "existing directories" + # so it's OK to overwrite the filemode='existing_directories' + # from above + filetypes='wav|ogg|mp3' + filemode='files' + fi + ;; + --profile|-p) + filemode='files' + ;; + --samplerate|-s) + # these are the ones suggested for zyn + # if you think more are required, + # remove this comment and write a justification + params='44100 48000 96000 192000' + ;; + --oversampling|-x) + params='1 2 4 8' + ;; + *) + local action_found + + # Is an action specified? + if [ "$cword" -gt 1 ] + then + local wrd + for wrd in "${COMP_WORDS[@]}" + do + # action named explicitly? + if _lmms_array_contains "$wrd" "${actions[@]}" + then + action_found=$wrd + break + # deprecated action name? + elif _lmms_array_contains "$wrd" "${actions_old[@]}" + then + action_found="$(_lmms_conv_old_action "$wrd")" + break + # no-action params found? + elif _lmms_array_contains "$wrd" "${pars_noaction[@]}" + then + action_found=none + break + fi + done + fi + + if [[ $prev =~ -e|--help|-h|-version|-v ]] + then + # the -e flag (from --import) and help/version + # always mark the end of arguments + return + fi + + if [[ "$action_found" =~ dump|none|^$ ]] && [[ $prev =~ \.mmpz? ]] + then + # mmp(z) mark the end of arguments for those actions + return + fi + + local savefiletypes='mmpz|mmp' + local params_array + + # find parameters/filetypes/dirtypes depending on actions + if ! [ "$action_found" ] + then + params_array=( "${actions[@]}" "${pars_global[@]}" "${pars_noaction[@]}") + filemode="existing_files" + filetypes="$savefiletypes" + elif [ "$action_found" == "none" ] + then + params_array=( "${pars_noaction[@]}" ) + filemode="existing_files" + filetypes="$savefiletypes" + elif [ "$action_found" == "dump" ] + then + filemode="existing_files" + filetypes="mmpz" + elif [ "$action_found" == "upgrade" ] + then + if [ "$prev" == "upgrade" ] + then + filemode="existing_files" + filetypes="$savefiletypes" + elif [ "$prev2" == "upgrade" ] + then + filemode="files" + filetypes="$savefiletypes" + fi + elif [[ "$action_found" =~ render(tracks)? ]] + then + if [[ "$prev" =~ render(tracks)? ]] + then + filemode="existing_files" + filetypes="$savefiletypes" + else + params_array=( "${pars_render[@]}" ) + fi + fi + + # add params_array to params, but also check the history of comp words + local param + for param in "${params_array[@]}" + do + local do_append=1 + for i in "${!COMP_WORDS[@]}" + do + if [ "$i" -ne 0 ] && [ "$i" -ne "$cword" ] + then + # disallow double long parameters + if [ "${COMP_WORDS[$i]}" == "$param" ] + then + do_append= + # disallow double short parameters + elif [ "--$(_lmms_long_param_of "${COMP_WORDS[$i]}")" == "$param" ] + then + do_append= + # --help or --version must be the first parameters + elif [ "$cword" -gt 1 ] && [[ $param =~ --help|--version ]] + then + do_append= + fi + fi + done + if [ "$do_append" ] + then + params+="$param " + fi + done + ;; + esac + + case $filemode in + + # use completion routine provided by bash-completion + # to fill $COMPREPLY + + existing_files) + _filedir "@($filetypes)" + ;; + + existing_directories) + _filedir -d + ;; + + files) + + # non existing files complete like directories... + _filedir -d + + # ...except for non-completing files with the right file type + if [ ${#COMPREPLY[@]} -eq 0 ] + then + if ! [[ "$cur" =~ /$ ]] && [ "$filetypes" ] && [[ "$cur" =~ \.($filetypes)$ ]] + then + # file ending fits, we seem to be done + COMPREPLY=( "$cur" ) + fi + fi + ;; + + esac + + if [ "$params" ] + then + # none of our parameters contain spaces, so deactivate shellcheck's warning + # shellcheck disable=SC2207 + COMPREPLY+=( $(compgen -W "${params}" -- "${cur}") ) + fi +} + +complete -F _lmms lmms