diff --git a/bash-tab-completion-occ.sh b/bash-tab-completion-occ.sh new file mode 100644 index 0000000000000..9feca3efcdd84 --- /dev/null +++ b/bash-tab-completion-occ.sh @@ -0,0 +1,310 @@ +#!/bin/bash + +## ########################################################################### +## Creates an alias to run `occ` as the appropriate user: +## the owner of nextcloud/config/config.php +## or +## a common web server user name found in /etc/passwd +## +## Optionally adds the alias to user's .bash_aliases file, if found +## If not found, optionally add alias to ~/.bashrc +## +## Optionally copies bash completion script `occ.bash` to +## ~/.local/share/bash-completion/completions/ +## +## @author Ronald Barnes +## @copyright Copyright 2022, Ronald Barnes ron@ronaldbarnes.ca +## ########################################################################### +## +## Usage: +## . bash-tab-completion-occ.sh +## or: +## source bash-tab-completion-occ.sh +## ########################################################################### + +## Save original "catch undefined vars" setting: +_occ_orig_set_u=$- +if [[ $_occ_orig_set_u =~ u ]] ; then + _occ_orig_set_u=0 +else + _occ_orig_set_u=1 +fi +## Catch undefined vars: +set -u + + +## Define colours: +function _occ_define_colours() + { + green="\e[1;32m" + yellow="\e[1;33m" + red="\e[1;31m" + default_colour="\e[00m" + } + + +## Leave no trace after exit (except alias(es)): +function cleanup_vars() + { + unset -v value + unset -v user_name + unset -v home_dir + unset -v _occ_alias_exists + unset -v php_found + unset -v answer + unset -v _occ_alias_string + unset -v _occ_alias_exists + + unset -v _occ_nc_path + unset -f _occ_get_nc_path + unset -v _occ_completion_script + unset -v script_found + unset -v _occ_script_installed + unset -v _occ_alias_installed + unset -v _occ_status + unset -v _occ_sudo_user + unset -f _occ_create_alias + + unset -v green + unset -v yellow + unset -v red + unset -v default_colour + + ## Reset all trap signals: + trap - SIGINT + trap - SIGHUP + trap - SIGTERM + trap - SIGKILL + trap - EXIT + trap - QUIT + unset -f cleanup_vars + ## Reset unbound var checking to original state: + if [[ ! _occ_orig_set_u -eq 0 ]] ; then + set +u + fi + unset -v _occ_orig_set_u + unset -f _occ_define_colours + unset -f _occ_bash_aliases + + ## End this program: + kill -s SIGINT $$ + ## End this program, second time if CTRL+C pressed in `read` / `readline`: + kill -s SIGINT $$ + } + + + + +function _occ_get_nc_path() + { + read -ep "Path to Nextcloud directory? " -i "/" _occ_nc_path + if [[ ! -f ${_occ_nc_path}/occ ]] ; then + _occ_get_nc_path + fi + } + + +function _occ_bash_aliases() + { + if [[ $# -ne 2 ]] ; then + ## Expecting path and file, i.e. ~ and .bash_aliases + return 99 + else + local home_dir=${1} + local alias_file=${2} + fi + + if [[ ! -w ${home_dir}/${alias_file} ]] ; then + return + fi + grep --quiet --no-messages "occ" ${home_dir}/${alias_file} + _occ_alias_exists=$? + if [[ _occ_alias_exists -eq 0 ]]; then + echo "There is an \"occ\" alias in ${home_dir}/${alias_file}:" + echo -n " --> " + grep --color=auto "occ" ${home_dir}/${alias_file} + _occ_alias_installed=0 + elif [[ -w ${home_dir}/${alias_file} ]]; then + echo -en "Add alias to " + echo -en "${yellow}${home_dir}/${alias_file}${default_colour}?" + read -s -p " (y/N) " -n 1 answer + if [[ ${answer} =~ ^Y|y ]] ; then + echo "Y" + echo "" >> ${home_dir}/${alias_file} + echo "## tab completion for Nextcloud:" >> ${home_dir}/${alias_file} + echo "alias occ=${_occ_alias_string}" >> ${home_dir}/${alias_file} + answer=$? + if [[ ${answer} -eq 0 ]] ; then + echo -ne "${green}Success${default_colour}: " + grep --color=auto occ ${home_dir}/${alias_file} + _occ_alias_installed=0 + fi + else + echo "N" + fi + fi + } + + + +function _occ_create_alias() + { + ## Note: `which` command not always installed, see if `php` exists this way: + php --version 1>/dev/null 2>/dev/null + php_found=$? + if [ $php_found -ne 0 ]; then + echo -e "${red}ERROR${default_colour}: php not found in path." + return 99 + fi + _occ_alias_string="'sudo --user ${_occ_sudo_user} php ${_occ_nc_path}/occ'" + echo -ne "Run \"${yellow}alias occ=" + echo -ne "${green}${_occ_alias_string}${default_colour}\"" + read -s -p " (Y/n)? " -n 1 answer + if [[ ${answer} =~ ^[Nn] ]] ; then + echo "N" + else + echo "Y" + eval alias "occ=${_occ_alias_string}" + alias occ + fi + } + + + + +## Capture exit conditions to clean up all variables: +trap 'cleanup_vars ALL' EXIT QUIT SIGINT SIGKILL SIGTERM + + +## Handy red / yellow / green / default colour defs: +_occ_define_colours + +user_name=$(whoami) +_occ_completion_script="occ.bash" + +## Find Nextcloud installation directory +_occ_nc_path=$(pwd) +if [[ ! -f ${_occ_nc_path}/occ ]] ; then + echo -e "Can't find ${yellow}occ${default_colour} in current directory." + _occ_get_nc_path + ## Strip trailing "/" from path: + _occ_nc_path=${_occ_nc_path%/} +fi + + +## Find owner of config/config.php, should be the web server user, per: +## https:/docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html#http-user-label +## but in shared hosting, might not be(?) +_occ_sudo_user="" +if [[ -f ${_occ_nc_path}/config/config.php ]] ; then + _occ_sudo_user=$(stat --format="%U" ${_occ_nc_path}/config/config.php) +else + echo -en "${red}WARNING${default_colour}: " + echo -en "Cannot locate ${yellow}${_occ_nc_path}/config/config.php" + echo -e "${default_colour}" + ## return 100 + read _occ_sudo_user <<< $(grep --only-matching --extended-regex \ + "www-data|httpd|nginx|lighttpd|apache|http|wwwrun" \ + /etc/passwd + ) +fi + +_occ_alias_string="" +## Looks for existing occ alias: +_occ_alias_string=$(alias occ 2>/dev/null) +_occ_alias_exists=$? +if [ ${_occ_alias_exists} -eq 0 ] ; then + echo "occ alias found for user \"${user_name}\":" + echo -e " --> ${green}$(alias occ)${default_colour}" + echo -en "Generate new alias? " + read -sp "(y/N) " -N 1 answer + if [[ ${answer} =~ ^[Yy] ]] ; then + echo "Y" + unalias occ + _occ_create_alias + else + echo "N" + fi +else + echo "No occ alias found for user \"${user_name}\"." + _occ_create_alias +fi + + +_occ_alias_installed=1 +## Is there an occ alias in ~/.bash_aliases? +_occ_bash_aliases ${HOME} ".bash_aliases" + +## If no alias installed into ~/bash_aliases file, try ~/.bashrc: +if [[ $_occ_alias_installed -ne 0 ]] ; then + _occ_bash_aliases ${HOME} ".bashrc" +fi + + +## Run ${_occ_completion_script} to handle bash auto completion? +script_found=1 ## aka False +if [[ -f ${_occ_nc_path}/${_occ_completion_script} ]] ; then + script_found=0 + echo -en "Run bash completion script " + echo -en "${green}${_occ_completion_script}${default_colour}? " + read -sp " (Y/n) " -N 1 answer + if [[ ${answer} =~ ^[Nn] ]] ; then + echo "N" + else + echo "Y" + echo -n "Running ${_occ_nc_path}/${_occ_completion_script} ... " + source ${_occ_nc_path}/${_occ_completion_script} + _occ_status=$? + if [[ ${_occ_status} -eq 0 ]] ; then + echo -e "${green}success${default_colour}." + else + echo -e "${red}Error${default_colour}." + fi + fi +else + echo -en "${red}WARNING${default_colour}: " + echo -en "Cannot find ${yellow}${_occ_nc_path}/${_occ_completion_script}" + echo -e "${default_colour}" +fi + + + +## Does ${_occ_completion_script} exist in... +## ~/.local/share/bash-completion/completions/? +_occ_script_installed=1 +if [[ -f ${_occ_nc_path}/${_occ_completion_script} ]] ; then + if [[ -r ~/.local/share/bash-completion/completions/${_occ_completion_script} ]] ; then + echo -en "Found ${yellow}~/.local/share/bash-completion/completions/" + echo -e "${_occ_completion_script}${default_colour}." + else + echo -en "Copy ${yellow}${_occ_completion_script}${default_colour} to " + echo -en "${yellow}~/.local/share/bash-completion/completions/" + echo -en "${default_colour}?" + read -sp " (y/N) " -n 1 answer + if [[ ${answer} =~ ^[Yy] ]] ; then + echo "Y" + mkdir -vp ~/.local/share/bash-completion/completions/ + ## File name MUST have name of command / alias it operates on when + ## it is in this location, i.e. occ, _occ, or occ.bash: + cp --verbose \ + --archive \ + --preserve=all \ + --interactive \ + ${_occ_nc_path}/${_occ_completion_script} \ + ~/.local/share/bash-completion/completions/ + ## If copy worked, chown and chmod for safety: + if [[ $? -eq 0 ]] ; then + chown -v root:root ~/.local/share/bash-completion/completions/occ.bash + chmod 0644 ~/.local/share/bash-completion/completions/occ.bash + fi + else + echo "N" + fi + fi +fi + + + +## Now clean all vars and remove all traps +cleanup_vars ALL +echo "DONE." diff --git a/build/files-checker.php b/build/files-checker.php index 3092771881140..501ec77c693f5 100644 --- a/build/files-checker.php +++ b/build/files-checker.php @@ -60,6 +60,7 @@ 'autotest-js.sh', 'autotest.sh', 'babel.config.js', + 'bash-tab-completion-occ.sh', 'build', 'composer.json', 'composer.lock', @@ -76,6 +77,7 @@ 'jest.config.js', 'lib', 'occ', + 'occ.bash', 'ocm-provider', 'ocs', 'ocs-provider', diff --git a/occ.bash b/occ.bash new file mode 100644 index 0000000000000..f94166177fef7 --- /dev/null +++ b/occ.bash @@ -0,0 +1,470 @@ +#!/bin/bash + +## ########################################################################### +## Companion script bash-tab-completion-occ.sh will install / invoke this script. +## +## bash-tab-completion-occ.sh or the user can copy this file to either +## /etc/bash_completion.d/ +## or +## ~/.local/share/bash-completion/completions/ +## so that it can get processed each time a new bash session is run. +## +## If copied to /etc/bash_completion.d/, then +## ensure it is owned by root:root (chown -v root:root thisFileName +## Also, only root can edit it: (chmod -v 0644 thisFileName) +## +## You may need to install bash-completion package, i.e.: +## sudo apt install bash-completion +## +## @author Ronald Barnes +## @copyright Copyright 2022, Ronald Barnes ron@ronaldbarnes.ca +## ########################################################################### +## +## NOTE: for best bash_completion experience, add the following lines to +## ~/.inputrc or /etc/inputrc: +## from "man readline": +set colored-completion-prefix on +## set colored-stats on + + + + + +## Get list of options beginning with "-", i.e. --help, --verbose, etc.: +function _occ_get_opts_list() + { + ## Check if passed name of app to run to get app-specific options: + local appName + if [[ $# -gt 0 ]] ; then + appName=${1} + fi + + ## Read the \n-delimited list into an array: + while read -r value; do + ## Don't display option if already on command line: + ## Use loop; word expansion via [@] does partial matches: + local X foundOpt="no" + for (( X=1 ; X<((${#COMP_WORDS[@]}-1)) ; X++ )) ; do + if [[ ${COMP_WORDS[$X]} == ${value} ]] ; then + foundOpt="yes" + fi + done + ## Don't add CWORD, it can be just "-": + if [[ ${foundOpt} == "no" && ${value} != ${CWORD} ]] ; then + opts_list+=("${value}") + fi + + ## Read output of "occ" and get word-chars following "-" or "--": + done <<< $(eval ${occ_alias_name} ${appName} | + grep --extended-regex --only-matching " \-(\-|\w)+" + ) + ## Verbose settings hard to parse: i.e. "-v|vv|vvv, --verbose" + ## Just manually add -vv and -vvv, and place in order (after -v): + ## Note: `complete` built-in handles de-duplication and sortation + opts_list+=("-vv") + opts_list+=("-vvv") + } + + + + + +## Get arguments and options for specific app, i.e. files:scan +function _occ_get_apps_args_and_opts() + { + local occCommand getOpts="no" length + if [[ $# -eq 0 ]] ; then + ## Expecting app to run + return 99 + else + occCommand="${@}" + fi + + ## Check if user is asking for opts; normally excluded: + if [[ $CWORD =~ "-" ]] ; then + getOpts="yes" + ## If a command was passed, it cannot END with "--", that means + ## "end of options, next is input" in bash. + ## So, strip "-" or "--" if they're CWORD: + if [[ $CWORD =~ "-"$ ]] ; then + occCommand=${@%%-*} + fi + fi + + local value + local args args_list + local opts opts_list + + ## Get all output into one var: + args=$(eval ${occ_alias_name} ${occCommand} --help --raw 2>&1) + ## Save a copy and avoid running occ twice (once for args, once for opts): + opts=args + + if [[ ${args} =~ Arguments: && $getOpts == no ]] ; then + ## Remove to and including Arguments: header: + args=${args#*Arguments:} + ## Remove Options: header to end of list: + args=${args%%Options:*} + ## Found arguments, read them to array: + while read -r value ; do + ## Strip trailing blanks: + value=${value%% *} + ## Skip blank lines: + if [[ ${value} =~ [:punct::alnum:]+ ]] ; then + args_list+=("${value}") + fi + done <<< ${args} + + ## If an arg is "app", get list of all apps, + if [[ ${args_list[@]} =~ "app" ]] ; then + ## Put list of apps into array for completing next input + _occ_get_apps_list + fi + + + ## if an arg is "name", get list of all config setting names, + ## OR, maybe it's dav looking for calendar name (dav:move-calendar): + if [[ ${args_list[@]} =~ "name" ]] ; then + if [[ ${args_list[@]} =~ "sourceuid" ]] ; then + ## If dav:calendar <(cal)name> , AND... + ## we already have a place-holder for <(cal) name>, then get user_id + ## NOTE: occ dav:move-calendar has count of THREE (empty 3rd placeholder) + if [[ ${#COMP_WORDS[@]} -eq 3 ]] ; then + _occ_get_calendar_names + else + _occ_get_users_list + fi + else + ## Put list of users into array for completing next input + _occ_get_setting_names + fi + ## If an arg is "user_id", get list of all users: + ## Note: occ dav:list-calendars expects uid, not user_id! + elif [[ ${args_list[@]} =~ "user_id" + || ${args_list[@]} =~ "uid" ]] ; then + ## Put list of users into array for completing next input + _occ_get_users_list + fi + + ## if an arg is "file", let `readline` defaults handle it: + if [[ ${args_list[@]} =~ "file" ]] ; then + compgen_skip="yes" + fi + + ## if an arg is "lang", get list of all languages FOR CHOSEN APP: + if [[ ${args_list[@]} =~ "lang" ]] ; then + ## Put list of languages into array for completing next input + _occ_get_languages_list + fi + fi + + if [[ $getOpts == yes ]] ; then + _occ_get_opts_list "${occCommand} --help" + display_args_arr+=(${opts_list[@]}) + fi + } + + +## If, i.e., dav:move-calendar [name] [sourceuid] [destinationuid]: +function _occ_get_calendar_names() + { + unset -v ${display_args_arr[@]} + _occ_get_users_list + local user_list=(${display_args_arr[@]}) + unset -v display_args_arr + + local value + ## Iterate through all users, gathering list of calendar names: + for (( X=0 ; X<${#user_list[@]} ; X++ )) ; do + while read -r value ; do + ## Strip trailing characters after and including "+" (table corners): + value=${value%%+*} + ## Strip leading table border & blanks: + value=${value#*| } + ## Strip trailing spaces to end of table: + value=${value%% *} + ## Don't include column header (uri) and blanks: + if [[ ! ${value} == uri && ! -z ${value} ]] ; then + display_args_arr+=("${value}") + fi + done <<< $(eval ${occ_alias_name} dav:list-calendars ${user_list[X]}) + done + } + + +## Put list of users into array for completing next input +function _occ_get_users_list() + { + local value + while read -r value ; do + ## Strip trailing characters after and including colon: + value=${value%%:*} + ## Strip leading blanks: + value=${value##* } + ## Don't include a user already on the command line: + if [[ ! ${COMP_WORDS[@]} =~ ${value} ]] ; then + display_args_arr+=("${value}") + fi + done <<< $(eval ${occ_alias_name} user:list) + } + + + +## Put list of apps into array for completing next input +function _occ_get_apps_list() + { + local args value + ## Only repopulate apps list if not done already in this instance: + if [[ -z ${occ_apps_list} ]] ; then + occ_apps_list=$(eval ${occ_alias_name} app:list) + fi + ## If Previous WORD was an app name, don't reload apps as completion options: + if [[ ! ${occ_apps_list[@]} =~ $PWORD ]] ; then + while read -r value ; do + ## Strip trailing characters after and including colon: + value=${value%%:*} + ## Strip leading blanks: + value=${value##* } + ## Skip "Enabled" and "Disabled" section headers: + if [[ ! (${value} == "Enabled" || ${value} == "Disabled") ]] ; then + display_args_arr+=("${value}") + fi + done <<< ${occ_apps_list} + fi + } + + + +## Put list of setting names into array for completing next input +function _occ_get_setting_names() + { + local value skip_until_app="yes" app_name=${PWORD} space_count=4 + + ## If last word is "--value", just return: no competion options to give: + if [[ ${PWORD} == --value ]] ; then + display_args_arr=() + return + fi + + ## Do not expect app name if, i.e. "config.system" has been typed: + if [[ ${PWORD} =~ "config:system" ]] ; then + skip_until_app="no" + app_name="system" + fi + + ## If occ config:system:set is 3 words ago (and Current WORD is blank), + ## only valid option is --value=: + ## i.e. occ config:system:set mail_domain [tab][tab] + ## length-3 length-2 length-1 + ## + local pword3=${#COMP_WORDS[@]} + pword3=$((pword3-3)) + ## + if [[ ( ${COMP_WORDS[$pword3]} == "config:system:set" + || ${COMP_WORDS[$((pword3-1))]} == "config:app:set" ) + && $CWORD == "" ]] ; then + display_args_arr=("--value") + return + + ## If occ config:app:get, then sub-options are 6 spaces from beginning: + elif [[ ${COMP_WORDS[$pword3]} == config:app:get ]] ; then + space_count=6 + skip_until_app="no" + + ## If occ config:app:set, then sub-options are 6 spaces from beginning: + elif [[ ${COMP_WORDS[$pword3]} == config:app:set ]] ; then + space_count=4, + skip_until_app="yes" + space_count=6 + skip_until_app="no" + fi + + while read -r value ; do + ## Strip trailing characters after and including colon: + value=${value%%:*} + ## Strip leading blanks: + value=${value##* } + ## Check if we've reached section header for app named $PWORD + if [[ ${value} == ${PWORD} ]] ; then + skip_until_app="no" + ## Skip until we've reached section header for app named $PWORD: + ## OR, a preceding app isn't necessary if config:system is in effect: + elif [[ ${skip_until_app} == "no" ]] ; then + display_args_arr+=("${value}") + fi + done <<< $(eval ${occ_alias_name} config:list ${app_name} --output plain | + grep --extended-regex --only-matching "^ {$space_count}- [[:alnum:]_.-]+" + ) + } + + + + +## If an app expects "lang" arg (language), then present completion list of +## file system entries so they can locate xx_YY.php +function _occ_get_languages_list() + { + ## NOTE: l18n:createjs expects: + ## app: an app name from app:list + ## lang: a PHP file, which user must provide: + ## PHP translation file + ## does not exist. + ## + ## Nothing can be done until PWORD is an app. + + ## If we have an apps list and the Previous WORD is in it... + if [[ -n ${occ_apps_list} && ${occ_apps_list} =~ ${PWORD} ]] ; then + ## Allow for file system completions: + compgen_skip="yes" + fi + } + + + + + + + + + + + + + + +## This runs the completions for "occ" command, which is an alias to: +## sudo -u $web_server_user php occ +function _occ() + { + ## When >1 NC installations, each should have own alias pointing to own + ## installation directory. + ## When fetching, say, app list, it's critical that correct alias invoked + ## i.e. Alias `occ` calls main NC v24 & alias `occbclug` calls NC v25 + occ_alias_name=${1} + + ## Word being inspected (CWORD==Current WORD, COMP_CWORD is index): + local CWORD=${COMP_WORDS[COMP_CWORD]} + ## Previous Word being inspected (PWORD==Previous WORD): + local PWORD=${COMP_WORDS[COMP_CWORD-1]} + + ## Change word break chars to exclude ":" since occ uses as separator: + COMP_WORDBREAKS="${COMP_WORDBREAKS/:/}" + + ## Default is word list, but files:scan, etc. require file name, + ## allow readline to handle that, per + ## 'https://stackoverflow.com/questions/12933362/getting-compgen-to-include-slashes-on-directories-when-looking-for-files/19062943#19062943' + compgen_skip="no" + compopt +o default + ## Put spaces after completed words: + compopt +o nospace + + + ## Put the options to present to user into this: + declare -a display_args_arr + + ## Parse out options (all start with "-" or "--"): + declare -a opts_list + + ## List of all args returned by running "occ", minus options that begin + ## with "-": + declare -a occ_args_arr + + ## temp storage to parse to array: + local occ_args + + ## List of apps available is used in multiple locations, + ## populate once, globally: + occ_apps_list="" + + ## Gather all valid args reported by running "occ": + while read -r occ_args ; do + if [[ ${occ_args} != command && ${occ_args:0:1} != - ]]; then + ## "command" is section heading, not valid arg: + ## Skip switches that begin with "-": + occ_args_arr+=("${occ_args}") + fi + done <<< $(eval ${occ_alias_name} --raw 2>&1| + ## Starts with alpha-numerics, can include colons, hyphens, _s: + grep --extended-regex --only-matching "^[[:alnum:]:_-]+" + ) + + ## If we're expecing a file path, let readline defaults handle it: + if [[ $CWORD == -p || $PWORD =~ "--path" || $CWORD =~ "--path" ]] ; then + compgen_skip="yes" + ## If ANY PREVIOUS word has colon, it's a command, run it and get its + ## arguments and options: + elif [[ ( ${COMP_WORDS[@]:1:((${#COMP_WORDS[@]}-1))} =~ : ) + ## Ensure that above condition does not include current word, + ## which can happen if index 0 == occ and index 1 == files:scan + ## Also, ensure we don't handle --path= here: + && ! ( ${CWORD} =~ : || ${PWORD} =~ = ) ]] ; then + local length=${#COMP_WORDS[@]}-1 + _occ_get_apps_args_and_opts "${COMP_WORDS[@]:1:length}" + ## Check for hyphen / dash / "-" as first char, if so, get options list: + elif [[ ${CWORD:0:1} == - ]] ; then + _occ_get_opts_list + display_args_arr=${opts_list[@]} + ## If this is first word (after "occ"), shorten list of valid choices + ## by stripping everything after first ":" + ## There can easily be 180+ options otherwise! + ## Note: there's initially an empty second array element after "occ": + ## Test for incomplete word by seeking colons: + elif [[ ! ${CWORD} =~ : ]]; then + ## Use associative array to get *unique* values: + declare -A short_list_args_arr + local X + for (( X=0 ; X<${#occ_args_arr[@]} ; X++ )); do + ## Here's the find/replace of colon -> end-of-line with just colon (:): + local value="${occ_args_arr[X]/:*/:}" + ## Here's the de-duplication via associate array: + short_list_args_arr[$value]=$value + done + display_args_arr=${short_list_args_arr[@]} + ## Since ":" is valid, and requires further sub-args, add NO SPACEs to end: + compopt -o nospace + else + ## So far, line consists of "occ ..." i.e. something more than "occ" + ## See what matches CWORD + local X + for (( X=0 ; X<${#occ_args_arr[@]} ; X++ )); do + ## Add only args that match current word: + if [[ ${occ_args_arr[X]} =~ ^${CWORD} ]] ; then + local value="${occ_args_arr[X]}" + display_args_arr+=("${value}") + fi + done + fi + + + local candidates=$( + compgen -W "${display_args_arr[*]}" -- "${CWORD}" + ) + ## When no matches, or an option takes a list of files, support + ## readline's default behaviour of completing files/directories: + if [ ${#candidates[@]} -eq 0 ] || [ "${compgen_skip}" == "yes" ]; then + compopt -o default + COMPREPLY=() + else + COMPREPLY=($(printf '%s' "${candidates[@]}")) + fi + + unset occ_apps_list + } + +complete -F _occ occ +## If there are multiple NC instances, then edit the +## ~/.bashrc or ~/.bash_aliases file +## and duplicate the alias that `bash-tab-completion-occ.sh` created so each has +## a unique name. +## +## Copy or symlink this file to a new name that matches the alias manually +## created in the previous step, saving it in the location: +## ~/.local/share/bash-completion/completions/ +## +## Then, in the new *.bash file, copy the `complete -F _occ` from above and +## finish it with the other alias name for each additional alias. +## +## Next, source the alias script (`. ~/.bash_aliases` or `source ~/.bashrc`). +## +## Finally, source the new script: +## `. $new_alias.bash` or `source $new_alias.bash`.