# winetricks(1) completion -*- shell-script -*- # # Copyright: # Copyright (C) 2018 Rob Walker # # License: # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later # version. # # 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. # # You should have received a copy of the GNU Lesser General Public # License along with this program. If not, see # . ##### Define Global constants / variables ##### WINETRICKS_PATH="$(command -v winetricks)" # https://pkgstore.datahub.io/core/country-list/data_csv/data/d7c9d7cfb42cb69f4422dec222dbbaa8/data_csv.csv COUNTRY_CODES="AF AX AL DZ AS AD AO AI AQ AG AR AM AW AU AT AZ BS BH BD BB BY BE BZ BJ BM BT BA BW BV BR IO BN BG BF BI \ KH CM CA CV KY CF TD CL CN CX CC CO KM CG CK CR CI HR CU CW CY CZ DK DJ DM DO EC EG SV GQ ER EE ET FK FO FJ FI FR \ GF PF TF GA GM GE DE GH GI GR GL GD GP GU GT GG GN GW GY HT HM VA HN HK HU IS IN ID IQ IE IM IL IT JM JP JE JO KZ KE KI KW KG \ LA LV LB LS LR LY LI LT LU MO MG MW MY MV ML MT MH MQ MR MU YT MX MC MN ME MS MA MZ MM NA NR NP NL NC NZ NI NE NG NU NF MP NO \ OM PK PW PA PG PY PE PH PN PL PT PR QA RE RO RU RW BL KN LC MF PM VC WS SM ST SA SN RS SC SL SG SX SK SI SB SO ZA GS SS ES LK \ SD SR SJ SZ SE CH SY TJ TH TL TG TK TO TT TN TR TM TC TV UG UA AE GB US UM UY UZ VU VN WF EH YE ZM ZW" INVERTIBLE_OPTS="isolate opt" TERMINATING_OPTS="help update version" TERMINATING_LIST_COMMAND="list" VERB_WINVER="winver=" COMMAND_WINEPREFIX="prefix=" ##### Define Global regular expression constants ##### BLANK_LINE_REGEX="^[[:blank:]]*$" VERB_REGEX="[[:alnum:]][-_[:alnum:]]*(|=[[:alnum:]][[:alnum:]]*)" METADATA_REGEX="^w_metadata[[:blank:]][[:blank:]]*[^\\\\]*[\\\\]\$" SHORT_OPTION_REGEX="[-]([[:alpha:]]|[[:alpha:]][[:alpha:]])" LONG_OPTION_REGEX="[-][-][[:lower:]][-_=*[:lower:]]*" FUNCTION_HANDLE_OPTIONS_REGEX="^winetricks_handle_option[(][)]$" FUNCTION_EXECUTE_COMMAND_REGEX="^execute_command[(][)]$" COMMAND_LINE_REGEX="^[[:blank:]][[:blank:]]*${VERB_REGEX}(|[=][*])[)]" REGULAR_VERB_LINE_REGEX="[[:blank:]](w_call|w_do_call)[[:blank:]]" DEPRECIATED_LINE_REGEX="[[:blank:]]w_warn[[:blank:]]" ##### Find and wrap a functioning awk variant ##### AWK="$(command -v awk 2>/dev/null || command -v mawk 2>/dev/null)" if [[ -z "${AWK}" ]] || "${AWK}" -W version 2>/dev/null | grep -q -E '^mawk 1\.3\.3'; then AWK="$(command -v gawk 2>/dev/null || command -v nawk 2>/dev/null)" [[ -z "${AWK}" ]] && AWK="$(command -v busybox)" && AWK="${AWK}${AWK:+ awk}" fi ##### Define shared awk functions ##### # _parse_and_dump_verbs() # # 1< array_verbs # > stdout # # awk function to parse an array of verbs or commands. # Assignment commands/ verbs are grouped in an array and parsed separately # from regular verbs, i.e. verbs of the form: # # videomem=512|1024|2048|default # # Dump output in the form: # # > # "verb(1) [... verb(M)] verb(M+1)=...|... [... verb(N)=...|...]" # awk_function_parse_and_dump_verbs=\ 'function _parse_and_dump_verbs(array_verbs, i, j, test_verb_prefix, verb_prefix, verb_suffix) { for (i=1 ; i<=array_verbs[0] ; ++i) { # Dump non-assignment verbs first... # Deleting these array entries as we go. if (array_verbs[i] !~ "=") { printf("%s ", array_verbs[i]) delete array_verbs[i] continue } # Process assignment verbs, by appending the suffix value to # an existing array entry (with the same prefix), and deleting # the current array entry... # Or if no existing, matching array entry exists, then simply leave # the existing array entry in place and take no further action. verb_prefix=verb_suffix=array_verbs[i] sub("=.*$", "", verb_prefix) sub("^.*=", "", verb_suffix) for (j=1; j stdout # # Dumps winetricks options, which can then be stored for later processing. # Parse the raw winetricks script, to increase processing and avoid any # calls to wine. # _scrape_options() { # shellcheck disable=SC2016 ${AWK} -vblank_line_regex="${BLANK_LINE_REGEX}" \ -vfunction_handle_options_regex="${FUNCTION_HANDLE_OPTIONS_REGEX}" \ -vshort_long_option_regex="^[[:blank:]][[:blank:]]*${SHORT_OPTION_REGEX}[|]${LONG_OPTION_REGEX}[)]" \ -vlong_option_regex="^[[:blank:]][[:blank:]]*${LONG_OPTION_REGEX}[)]" \ '{ in_function=in_function || ($0 ~ function_handle_options_regex) is_blank=($0 ~ blank_line_regex) is_comment=($0 ~ "^[[:blank:]]*#") if (!in_function || is_blank || is_comment) next if (($0 ~ long_option_regex) || ($0 ~ short_long_option_regex)) { gsub("(^[[:blank:]]*|[)][^)]*$)" , "") printf("%s\n", $0) } in_function=($0 !~ "^[}]$") if (!in_function) exit 0 }' "${WINETRICKS_PATH}" 2>/dev/null } # _scrape_commands() # (< VERB_REGEX, WINETRICKS_RAW_COMMANDS) # > stdout # # Parse list of commands (excluding options) directly from the # winetricks script. Dump all regular commands, followed by # a list of assignment commands (i.e. prefix=*) # # > # "command(1) [... command(M)] command(M+1)=...|... [... command(N)=...|...]" # _scrape_commands() { # shellcheck disable=SC2016 ${AWK} -vfunction_execute_command_regex="${FUNCTION_EXECUTE_COMMAND_REGEX}" \ -vcommand_line_regex="${COMMAND_LINE_REGEX}" \ -vregular_verb_line_regex="${REGULAR_VERB_LINE_REGEX}" \ -vdepreciated_line_regex="${DEPRECIATED_LINE_REGEX}" \ "${awk_function_parse_and_dump_verbs}"' { in_function=in_function || ($0 ~ function_execute_command_regex) if (!in_function) next in_function=($0 !~ "^[}]$") if (!in_function) exit 0 is_command=($0 ~ command_line_regex) is_warning=($0 ~ depreciated_line_regex) is_normal_verb=($0 ~ regular_verb_line_regex) if (!is_command || is_warning || is_normal_verb) next gsub("(^[[:blank:]][[:blank:]]*|[)].*)", "") if ($0 == "*=*") next array_commands[++array_commands[0]]=$0 } END{ _parse_and_dump_verbs(array_commands) }' "${WINETRICKS_PATH}" 2>/dev/null } # _scrape_all_categories_and_verbs() # (< METADATA_REGEX, WINETRICKS_RAW_METADATA) # > stdout # # Parse winetricks list-all to get sets of all categories and # the verbs contained within that category. # NB the BASH completion script copes with jumbled up category # blocks in the main winetricks script (i.e. blocks of one category # that are interspersed with other categories). # # > # "category(1) # verb(1) [... verb(N)] # category(2) # verb(1) [... verb(N)] # ..." # _scrape_all_categories_and_verbs() { # shellcheck disable=SC2016 ${AWK} -vmetadata_regex="${METADATA_REGEX//\\/\\\\}" \ "${awk_function_parse_and_dump_verbs}"' { if (($0 !~ metadata_regex) || (NF < 3)) next new_verb=$2 new_category=$3 if (category != new_category) { if (array_verbs[0]) { printf("%s\n", category) _parse_and_dump_verbs(array_verbs) } category=new_category delete array_verbs } array_verbs[++array_verbs[0]]=new_verb } END{ if (array_verbs[0]) { printf("%s\n", category) _parse_and_dump_verbs(array_verbs) } }' "${WINETRICKS_PATH}" 2>/dev/null } ##### Define general BASH helper functions ##### # _list_remove_items() # 1-N< search-term1 [... search-termN] # < stdin (list) # > stdout (list) # # Takes a list of items and parses items to stdout. # Removing any items matching the supplied 'search-term'(s). # _list_remove_items() { (($#>=1)) || return 1 local found i item search_term while read -r -d ' ' item || [[ -n "${item}" ]]; do [[ -z "${item}" ]] && continue found=0 for ((i=1 ; i<=$# ; ++i)); do # shellcheck disable=SC2124 search_term="${@:i:1}" [[ -z "${search_term}" ]] && continue if [[ "${item}" = "${search_term}" ]]; then found=1 break fi done if ((!found)); then printf "%s " "${item}" fi done } # _list_remove_regex_item() # 1< regular-expression # < stdin (list) # > stdout (list) # # Takes a list of items and parses items to stdout. # Removing any items matching 'regular-expression'. # _list_remove_regex_item() { (($#==1)) || return 1 local regex_search_term="${1}" item while read -r -d ' ' item || [[ -n "${item}" ]]; do if [[ ! ( -z "${item}" || "${item}" =~ ${regex_search_term} ) ]]; then printf "%s " "${item}" fi done } # _list_match_item() # 1 < search-text # [2-N]< items to match against (list) # > match? # # Match list of items on stdin with 'search-text', # # Return 0=match / 1=no match # _list_match_item() { (($#>=1)) || return 1 [[ -z "${1}" ]] && return 1 local search_text="${1}" shift 1 while [[ ! -z "${1}" ]]; do [[ "${search_text}" = "${1}" ]] && return 0 shift 1 done return 1 } # _get_duplicate_options() # 1-N< Winetricks options (multi-line list) # (< INVERTIBLE_OPTS) # > stdout # # For each long option determine if it has a matching short option or if # the long option is invertible (i.e. "--no-xxxx" vs "--xxxx"). # # > # "[short-option(1)|inverted-long-option(1)] long-option(1) # ... # [short-option(N)|inverted-long-option(N)] long-option(N)" # _get_duplicate_options() { (($#>=1)) || return 1 local first_item item option options invertible_option \ invertible_options=" ${INVERTIBLE_OPTS} " \ removed_options="" while IFS='' read -r options || [[ -n "${options}" ]]; do options=" ${options//|/ } " for option in ${removed_options}; do options="${options// ${option} / }" done [[ "${options}" =~ ${BLANK_LINE_REGEX} ]] && continue unset -v negated_duplicate # shellcheck disable=SC2016 first_item="$( echo "${options}" | ${AWK} '{print $1}' 2>/dev/null )" for invertible_option in ${invertible_options}; do case "${invertible_option}" in isolate) negated_duplicate="${first_item/#--no-${invertible_option}/--${invertible_option}}" [[ "${negated_duplicate}" = "${first_item}" ]] || break negated_duplicate="${first_item/#--${invertible_option}/--no-${invertible_option}}" [[ "${negated_duplicate}" = "${first_item}" ]] || break ;; opt) negated_duplicate="${first_item/%${invertible_option}out/${invertible_option}in}" [[ "${negated_duplicate}" = "${first_item}" ]] || break negated_duplicate="${first_item/%${invertible_option}in/${invertible_option}out}" [[ "${negated_duplicate}" = "${first_item}" ]] || break ;; esac done if [[ ! -z "${negated_duplicate}" && ( ! "${negated_duplicate}" = "${first_item}" ) ]]; then options="${options} ${negated_duplicate}" removed_options="${removed_options} ${first_item} ${negated_duplicate}" invertible_options="${invertible_options// ${invertible_option} / }" fi [[ "${options}" =~ ${BLANK_LINE_REGEX} ]] && continue printf " %s \\n" "${options}" done <<<"${@}" } # _process_options() # (< COUNTRY_CODES) # 1< Winetricks options (multi-line list) # > stdout # # Process winetricks options. Format these into a list of short options # followed by a list of long options. Expands country codes assignment # variable arguments. # # > # "short-option(1) [... short-option(N)] long-option(1) [... long-option(N)]" # _process_options() { (($#==1)) || return 1 [[ -z "${1}" ]] && return 1 local line long_options option short_options while IFS='' read -r line || [[ -n "${line}" ]]; do while read -r -d '|' option || [[ -n "${option}" ]]; do case "${option}" in --*) if [[ ! "${option}" = "${option/country/}" ]]; then option="${option%=*}=${COUNTRY_CODES// /|}" fi long_options="${long_options} ${option}" ;; -*) short_options="${short_options} ${option}" ;; esac done <<<"${line}" done <<<"${1}" printf "%s\\n" "${short_options}${long_options}" } # _get_categories() # ( < CATEGORIES_VERBS_LIST ) # > stdout # # Get a list of categories. # # > # "category(1) [... category(N)]" # _get_categories() { local categories="" is_category=1 line while IFS='' read -r line || [[ -n "${line}" ]]; do if ((is_category)) && [[ ! "${categories}" =~ ${line} ]]; then categories="${categories} ${line}" fi is_category=$((!is_category)) done <<<"${CATEGORIES_VERBS_LIST}" [[ -z "${categories}" ]] || printf "%s " "${categories}" } # _get_category_verbs() # ( < CATEGORIES_VERBS_LIST ) # [1< category] # > stdout # # For the specified category, print a line of all verbs in that category. # If no category is specified then print a line of all verbs, for all categories. # # > # "verb(1) [... verb(N)]" # _get_category_verbs() { local category="${1:-}" is_category=1 line matched while IFS='' read -r line || [[ -n "${line}" ]]; do if ((is_category)); then matched=0 if [[ -z "${category}" || ( "${category}" = "${line}" ) ]]; then matched=1 fi elif ((matched)); then printf " %s " "${line}" fi is_category=$((!is_category)) done <<<"${CATEGORIES_VERBS_LIST}" } # _assignment_strip_values() # 1< new line: 1= TRUE , 0=FALSE (for all items) # < stdin # > stdout # # Takes a list of verbs or options and processes assignments, # which use the "=" assignment operator. # Remove the post-assignment regex values. # # < # "... videomem=default|512|1024|2048 ..." # > # "... videomem= ..." # _assignment_strip_values() { (($#==1)) || return 1 # shellcheck disable=SC2016 ${AWK} -vnew_line="${1}" \ '{ gsub("=[^[:blank:]]*","=") for (i=1 ; i<=NF ; ++i) printf("%s%s", $i, new_line ? "\n" : " ") }' 2>/dev/null } # _assignment_get_values() # 1< target # < stdin # > stdout # # Takes a list of verbs or options and processes a single specified target assignment. # Remove the prefix target name. # Convert the postfix regex values to a simple WS separated list. # # 1< target="videomem" # < # "... videomem=default|512|1024|2048 ..." # > # "default 512 1024 2048" # _assignment_get_values() { (($#==1)) || return 1 # shellcheck disable=SC2016 ${AWK} -vtarget_prefix="${1}" \ '{ for (i=1 ; i<=NF ; ++i) { verb_prefix=verb_suffix=verb=$i if (verb !~ "=") continue sub("=.*$", "", verb_prefix) sub("^.*=", "", verb_suffix) if (verb_prefix != target_prefix) continue gsub("[|]", " ", verb_suffix) printf("%s\n", verb_suffix) } }' } # _get_alternate_opt() # (< DUP_OPTS_LIST) # 1< search-option # > stdout (alternate-option) # # Given the specified 'search-option' find the alternate # (or inverse) operation, if one exists. # Print this to stdout. # # < option="--no-isolate" # > # "--isolate" # _get_alternate_opt() { (($#==1)) || return 1 local search_opt="${1}" alt_opt dummy line opt while IFS='' read -r line || [[ -n "${line}" ]]; do # shellcheck disable=SC2034 read -r opt alt_opt dummy<<<"${line}" || continue [[ -z "${opt}" || -z "${alt_opt}" ]] && continue if [[ "${opt}" = "${search_opt}" ]]; then printf "%s\\n" "${alt_opt}" elif [[ "${alt_opt}" = "${search_opt}" ]]; then printf "%s\\n" "${opt}" fi done <<<"${DUP_OPTS_LIST}" } # _get_opt_regex_matches() # (< OPTS, DUP_OPTS_LIST) # 1-N < regex [... regex] # > stdout # # Takes a list of regular expressions. # Match all available options, including duplicate / inverse options, # against each regular expression. # Dump all matching options to stdout. # # < "help" "update" # > # " -h --help --self-update --update-rollback " # _get_opt_regex_matches() { (($#>=1)) || return 1 local i line opts_list search_regex # shellcheck disable=SC2207 opts_list="$( echo "${OPTS}" | _assignment_strip_values 1 )" opts_list="${opts_list} ${DUP_OPTS_LIST}" while IFS='' read -r line || [[ -n "${line}" ]]; do for ((i=1 ; i<=$# ; ++i)); do # shellcheck disable=SC2124 search_regex="${@:i:1}" if [[ "${line}" =~ ${search_regex} ]]; then printf "%s " "${line}" fi done done <<<"${opts_list}" printf " \\n" } # _cull_duplicate_options() # 1 < option to remove # 2-N < option list to process # > stdout # # Takes an option to remove, from an list of options. # Note: also removes duplicate or inverse option(s) (if any exist). # _cull_duplicate_options() { (($#>=1)) || return 1 local opt="${1}" alt_other_alt_opt alt_opt other_alt_opt shift 1 # Ignore non-option verbs, not starting with '-' if [[ "${opt#-}" == "${opt}" ]]; then echo "${@}" return fi alt_opt="$( _get_alternate_opt "${opt}" )" # Try to remove additional duplicate operations of the form: # --really-verbose = --verbose / -vv = -v # shellcheck disable=SC1001 if [[ "${opt}" =~ ^\-[[:alpha:]][[:alpha:]]$ ]]; then other_alt_opt="${opt%?}" else other_alt_opt="${opt/really-/}" fi alt_other_alt_opt="$( _get_alternate_opt "${other_alt_opt}" )" echo "${@}" | _list_remove_items "${opt}" "${alt_opt}" "${other_alt_opt}" "${alt_other_alt_opt}" } ##### Define main BASH completion function ##### _winetricks() { local cur i opt test_cur test_prev reply temp_opts temp_verbs terminating_opts cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=( ) # shellcheck disable=SC2086 if _list_match_item "${prev}" ${CATEGORIES_LIST}; then # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "${TERMINATING_LIST_COMMAND}" -- "${cur}") ) return 0 fi # Handle the special case when a Wineprefix path needs to be specified. for ((i=(COMP_CWORD-1) ; i>=(COMP_CWORD-2) ; --i)); do ((i < 0)) && continue if [[ "${COMP_WORDS[i]}=" = "${COMMAND_WINEPREFIX}" ]]; then _filedir -d return 0 fi done # Disable tab expansion when a terminating option or command has been typed... # shellcheck disable=SC2086 terminating_opts="$(_get_opt_regex_matches ${TERMINATING_OPTS})" # shellcheck disable=SC2086 if _list_match_item "${prev}" ${terminating_opts} ${COMMANDS} ${TERMINATING_LIST_COMMAND}; then # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "" -- "") ) return 0 fi # When an option or verb is specified then disable from being repeated via tab expansion. # For options also handle any duplicate and inverse/negated variants. # NB we don't touch the global OPTS / ALL_VERBS variables. If the user starts a new line # or deletes a term, then we must be able to revert back to the default verbs / options. temp_opts="${OPTS}" temp_verbs="${ALL_VERBS}" for ((i=1 ; i<=(COMP_CWORD-1) ; ++i)); do # shellcheck disable=SC2086 temp_opts="$( _cull_duplicate_options "${COMP_WORDS[i]}" ${temp_opts} )" temp_verbs="${temp_verbs// ${COMP_WORDS[i]} / }" done # Parse assignment verbs and options, of the form 'videomem=' for ((i=(COMP_CWORD-1) ; i<=COMP_CWORD ; ++i)); do ((i<1)) && continue test_cur="${COMP_WORDS[i]}" test_prev="${COMP_WORDS[i-1]}" if [[ ( "${test_cur}" != "=" ) || ( "${test_prev}=" == "${VERB_WINVER}" ) ]]; then continue fi case "${test_prev}" in -*) reply="${temp_opts}" ;; *) reply="${COMMANDS} ${temp_verbs}" ;; esac # If the reply contains any assignment operations matching the previous term # then strip the first part of the verb (e.g. 'videomem=') from the reply term. # Replace this with all the possible assignment values, e.g.: '1024 2048 512 default'. reply="$( echo "${reply}" | _assignment_get_values "${test_prev}" )" # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "${reply}" -- "${cur%=}") ) # Suppress starting a new argument when have an assignment option or command... if [[ "${COMPREPLY[0]%=}=" = "${COMPREPLY[0]}" ]]; then compopt -o nospace fi return 0 done # Parse general (non-assignment) verbs and options. # Only display all available short options when the end user tab completes a hyphen character. # Only display all available long options when the end user tab completes a pair of hyphen characters. # Reduces end-user overload! case "${cur}" in --*) reply="$( echo "${temp_opts}" | _assignment_strip_values 0 )" ;; -*) reply="$( echo "${temp_opts}" | _list_remove_regex_item "${LONG_OPTION_REGEX}" )" ;; *) reply="$( echo "${COMMANDS}" "${temp_verbs}" | _assignment_strip_values 0 )" ;; esac # shellcheck disable=SC2207 COMPREPLY=( $(compgen -W "${reply}" -- "${cur}") ) if [[ "${COMPREPLY[0]}" = "${VERB_WINVER}" ]]; then return 0 fi # Suppress starting a new argument when have an assignment option or command... if [[ "${COMPREPLY[0]%=}=" = "${COMPREPLY[0]}" ]]; then compopt -o nospace fi return 0 } # We've found no winetricks script in the end-user's PATH, so give up now ... [[ -z "${WINETRICKS_PATH}" || ! -f "${WINETRICKS_PATH}" ]] && return 1 # We've tried real hard, but the end-user doesn't appear to have a working awk implementation... [[ -z "${AWK}" ]] && return 1 # Setup various Global variables (when this script is initially source'd)... CATEGORIES_VERBS_LIST="$(_scrape_all_categories_and_verbs)" RAW_OPTIONS="$(_scrape_options)" COMMANDS="$(_scrape_commands)" CATEGORIES_LIST="$(_get_categories)" COMMANDS="${COMMANDS} ${CATEGORIES_LIST}" OPTS="$(_process_options "${RAW_OPTIONS}")" DUP_OPTS_LIST="$(_get_duplicate_options "${RAW_OPTIONS}")" # Convert a multi-line list of all categories and verbs into a single line of verbs... ALL_VERBS="$(_get_category_verbs "")" complete -F _winetricks winetricks