#!/bin/bash # # SPDX-FileCopyrightText: 2021-2025 BlueALSA developers # SPDX-License-Identifier: MIT # # Bash completion for bluez-alsa project applications. # helper function gets available profiles # @param $1 the bluealsad executable name _bluealsa_profiles() { "$1" --help | while read -r line; do [[ "$line" = "Available BT profiles:" ]] && start=yes && continue [[ "$start" ]] || continue [[ "$line" ]] || break read -ra words <<< "$line" echo "${words[1]}" done } # helper function gets available codecs # @param $1 the bluealsad executable name _bluealsa_codecs() { "$1" --help | while read -r line; do [[ "$line" = "Available BT audio codecs:" ]] && start=yes && continue [[ "$start" ]] || continue [[ "$line" ]] || break line=${line,,} IFS=", " read -ra words <<< "${line,,}" echo "${words[@]:1}" done } # helper function gets available enum values # @param $1 the bluealsad executable name # @param $2 the bluealsad option to inspect _bluealsa_enum_values() { "$1" "${2}=" 2>&1 | while read -r line; do [[ $line =~ \{([^}]*)\} ]] || continue echo "${BASH_REMATCH[1]//,/}" done } # helper function gets dbus services # note that this function does not detect if default service (no suffix) is running. _bluealsa_list_dbus_suffices() { dbus-send --system --dest=org.freedesktop.DBus --type=method_call \ --print-reply /org/freedesktop/DBus \ org.freedesktop.DBus.ListNames 2>/dev/null | \ while read -r line; do [[ $line =~ org\.bluealsa\.([^'"']+) ]] || continue echo "${BASH_REMATCH[1]}" done } # helper function gets codecs for given pcm # before calling this function, make sure that the bautil_args was properly # initialized with the _bluealsa_util_init function # @param $1 pcm path _bluealsa_pcm_codecs() { "${bautil_args[@]}" codec "$1" | while read -r line; do [[ $line =~ ^Available\ codecs:\ ([^[]+$) ]] || continue echo "${BASH_REMATCH[1]}" done } # helper function completes supported bluealsactl monitor properties _bluealsactl_properties() { local properties=( codec running softvolume volume ) if [[ "$cur" == *,* ]]; then local realcur prefix chosen remaining realcur="${cur##*,}" prefix="${cur%,*}" IFS="," read -ra chosen <<< "${prefix,,}" readarray -t remaining < <(printf '%s\n' "${properties[@]}" "${chosen[@]}" | sort | uniq -u) if [[ ${#remaining[@]} -gt 0 ]]; then readarray -t COMPREPLY < <(compgen -W "${remaining[*]}" -- "$realcur") if [[ ${#COMPREPLY[@]} -eq 1 ]] ; then COMPREPLY[0]="$prefix,${COMPREPLY[0]}" fi if [[ ${#remaining[@]} -gt 0 && "$cur" == "${COMPREPLY[0]}" ]] ; then COMPREPLY=( "${COMPREPLY[0]}," ) fi if [[ ${#remaining[@]} -gt 1 ]]; then compopt -o nospace fi fi else readarray -t COMPREPLY < <(compgen -W "${properties[*]}" -- "$cur") if [[ ${#COMPREPLY[@]} -eq 1 && "$cur" == "${COMPREPLY[0]}" ]]; then COMPREPLY=("${COMPREPLY[0]},") fi compopt -o nospace fi } # helper function gets ALSA pcms # @param $1 is the current word to match _bluealsa_aplay_pcms() { eval local cur="$1" cur="${cur/%\\/\\\\}" while read -r; do [[ "$REPLY" == " "* ]] && continue [[ "$REPLY" == "$cur"* ]] || continue echo "${REPLY// /\\ }" done < <(aplay -L 2>/dev/null) } # helper function gets rfcomm dbus paths # @param $1 the full BlueALSA service name ( org.bluealsa* ) _bluealsa_rfcomm_paths() { busctl --list tree "$1" 2>/dev/null | while read -r line; do [[ "$line" = /org/bluealsa/hci[0-9]/dev*/rfcomm ]] || continue echo "${line/\/rfcomm/}" done } # helper function gets options from command line # @param $1 the executable name # puts exec and dbus arg ( --dbus=aaa ) into array variable bautil_args # puts service name ( org.bluealsa.aaa ) into variable service # puts offset of first non-option argument into variable nonopt_offset # @return 0 if no errors found, 1 if dbus check failed _bluealsa_util_init() { local valopts="-B --dbus" bautil_args=( "$1" ) service="org.bluealsa" nonopt_offset=0 local i for (( i=1; i <= COMP_CWORD; i++ )); do case "${COMP_WORDS[i]}" in --dbus) if (( i == COMP_CWORD )) ; then break elif (( i == COMP_CWORD - 1 )) ; then [[ "${COMP_WORDS[i+1]}" = = ]] && break bautil_args+=( "--dbus=${COMP_WORDS[i+1]}" ) service="org.bluealsa.${COMP_WORDS[i+1]}" break else [[ "${COMP_WORDS[i+1]}" = = ]] && (( i++ )) if [[ "${COMP_WORDS[i+1]}" ]] ; then (( i++ )) bautil_args+=( "--dbus=${COMP_WORDS[i]}" ) service="org.bluealsa.${COMP_WORDS[i]}" continue fi fi ;; -B) if (( i == COMP_CWORD )) ; then break elif (( i == COMP_CWORD - 1 )) ; then bautil_args+=( "--dbus=${COMP_WORDS[i+1]}" ) service="org.bluealsa.${COMP_WORDS[i+1]}" break else if [[ "${COMP_WORDS[i+1]}" ]] ; then (( i++ )) bautil_args+=( "--dbus=${COMP_WORDS[i]}" ) service="org.bluealsa.${COMP_WORDS[i]}" continue fi fi ;; -*|=) continue ;; esac [[ "${COMP_WORDS[i-1]}" == = ]] && (( i < COMP_CWORD )) && continue [[ "$valopts " == *"${COMP_WORDS[i-1]} "* ]] && continue nonopt_offset=$i break done return 0 } # helper function completes options _bluealsa_complete_options() { readarray -t COMPREPLY < <(compgen -W "$(_parse_help "$1")" -- "$cur") [[ ${COMPREPLY[0]} == *= ]] && compopt -o nospace } # completion function for bluealsad # complete available devices and profiles in addition to options _bluealsad() { local cur prev words cword split _init_completion -s || return local prefix list case "$prev" in --device|-i) readarray -t list < <(ls -I '*:*' /sys/class/bluetooth) readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") return ;; --profile|-p) [[ $cur =~ ^[+-].* ]] && prefix=${cur:0:1} && cur=${cur:1} readarray -t list < <(_bluealsa_profiles "$1") readarray -t COMPREPLY < <(compgen -P "$prefix" -W "${list[*]}" -- "$cur") return ;; --codec|-c) [[ $cur =~ ^[+-].* ]] && prefix=${cur:0:1} && cur=${cur:1} readarray -t list < <(_bluealsa_codecs "$1") readarray -t COMPREPLY < <(compgen -P "$prefix" -W "${list[*]}" -- "$cur") return ;; --loglevel|--sbc-quality|--aac-latm-version|--mp3-algorithm|--mp3-vbr-quality|--ldac-quality) readarray -t list < <(_bluealsa_enum_values "$1" "$prev") readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") return ;; --xapl-resp-name) readarray -t COMPREPLY < <(compgen -W "BlueALSA iPhone" -- "$cur") return ;; --*) [[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return ;; esac _bluealsa_complete_options "$1" } # completion function for bluealsa-aplay # completes available dbus suffices and ALSA pcms in addition to options # - does not complete MAC addresses # requires aplay to list ALSA pcms _bluealsa_aplay() { local cur prev words cword split _init_completion -s -n : || return local list case "$prev" in --dbus|-B) _have dbus-send || return readarray -t list < <(_bluealsa_list_dbus_suffices) readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") return ;; --pcm|-D) _have aplay || return # do not attempt completion on words containing ' or " [[ "$cur" =~ [\'\"] ]] && return readarray -t COMPREPLY < <(_bluealsa_aplay_pcms "$cur") # ALSA pcm names can contain '=' and ':', both of which cause # problems for bash completion if it considers them to be word # terminators. So we adjust the list of candidate matches to allow # for this. if [[ "$cur" == *=* && "$COMP_WORDBREAKS" == *=* ]]; then # Remove equal-word prefix from COMPREPLY items local equal_prefix=${cur%"${cur##*=}"} local i=${#COMPREPLY[*]} while [[ $((--i)) -ge 0 ]]; do COMPREPLY[i]=${COMPREPLY[$i]#"$equal_prefix"} done fi __ltrim_colon_completions "$cur" return ;; --loglevel|--resampler|--volume) readarray -t list < <(_bluealsa_enum_values "$1" "$prev") readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") return ;; --*) [[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return ;; esac _bluealsa_complete_options "$1" } # completion function for bluealsactl # complete available dbus suffices, command names and pcm paths in addition to options _bluealsactl() { local cur prev words cword split _init_completion -s || return local bautil_args service local -i nonopt_offset _bluealsa_util_init "$1" || return # the command names supported by this version of bluealsactl local simple_commands="list-pcms list-services monitor status" local path_commands="codec client-delay info mute open soft-volume volume" # options that may appear before or after the command local global_shortopts="-h" local global_longopts="--help" # options that may appear only before the command local base_shortopts="-B -V -q -v" local base_longopts="--dbus= --version --quiet --verbose" local command path list # process pre-command options case "$nonopt_offset" in 0) case "$prev" in --dbus|-B) _have dbus-send || return readarray -t list < <(_bluealsa_list_dbus_suffices) readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") return ;; esac case "$cur" in -|--*) readarray -t COMPREPLY < <(compgen -W "$global_longopts $base_longopts" -- "$cur") [[ "${COMPREPLY[0]}" == *= ]] && compopt -o nospace ;; -?) readarray -t COMPREPLY < <(compgen -W "$global_shortopts $base_shortopts" -- "$cur") ;; esac return ;; "$COMP_CWORD") # list available commands readarray -t COMPREPLY < <(compgen -W "$simple_commands $path_commands" -- "$cur") return ;; esac # check for valid command command="${COMP_WORDS[nonopt_offset]}" [[ "$simple_commands $path_commands" =~ $command ]] || return # process command-specific options case "$command" in monitor) case "$prev" in --properties) _bluealsactl_properties return ;; esac global_longopts+=" --properties" global_shortopts+=" -p" case "$cur" in -p) COMPREPLY=( "--properties" ) compopt -o nospace return ;; -p?*) COMPREPLY=( "--properties=${cur:2}" ) compopt -o nospace return ;; --properties) COMPREPLY=( "--properties=" ) compopt -o nospace return ;; -) COMPREPLY=( "--" ) compopt -o nospace return ;; esac ;; esac # find path argument for (( i=(nonopt_offset + 1); i < COMP_CWORD; i++ )); do [[ "${COMP_WORDS[i]}" == -* ]] && continue path="${COMP_WORDS[i]}" path_offset=i break done # process global options and path if [[ -z "$path" ]] ; then case "$cur" in -|--*) readarray -t COMPREPLY < <(compgen -W "$global_longopts" -- "$cur") [[ "${COMPREPLY[0]}" == --properties* ]] && compopt -o nospace return ;; -?) readarray -t COMPREPLY < <(compgen -W "$global_shortopts" -- "$cur") return ;; esac if [[ "$path_commands" =~ $command ]]; then readarray -t list < <("${bautil_args[@]}" list-pcms 2>/dev/null) readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") return fi fi # process command positional arguments case "$command" in codec) if (( COMP_CWORD == path_offset + 1 )) ; then readarray -t list < <(_bluealsa_pcm_codecs "$path") readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") fi ;; mute) if (( COMP_CWORD < path_offset + 3 )) ; then if [[ "$cur" == "" ]] ; then COMPREPLY=( true false ) else readarray -t COMPREPLY < <(compgen -W "on yes true 1 off no false 0" -- "${cur,,}") fi fi ;; soft-volume) if (( COMP_CWORD < path_offset + 2 )) ; then if [[ "$cur" == "" ]] ; then COMPREPLY=( true false ) else readarray -t COMPREPLY < <(compgen -W "on yes true 1 off no false 0" -- "${cur,,}") fi fi ;; monitor) case "$prev" in --properties) _bluealsactl_properties ;; -p) return ;; esac ;; esac } # completion function for bluealsa-rfcomm # complete available dbus suffices and device paths in addition to options # requires busctl (part of elogind or systemd) to list device paths _bluealsa_rfcomm() { local cur prev words cword split _init_completion -s || return local bautil_args service local -i nonopt_offset _bluealsa_util_init "$1" || return local list # check for dbus service suffix first case "$prev" in --dbus|-B) _have dbus-send || return readarray -t list < <(_bluealsa_list_dbus_suffices) readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") return ;; --*) [[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return ;; esac if (( nonopt_offset == COMP_CWORD )) ; then _have busctl || return readarray -t list < <(_bluealsa_rfcomm_paths "$service") readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur") return fi # do not list options if path was found (( nonopt_offset > 0 )) && return _bluealsa_complete_options "$1" } # completion function for a2dpconf and hcitop # complete only the options _bluealsa_others() { # shellcheck disable=SC2034 local cur prev words cword split _init_completion -s || return case "$prev" in --*) [[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return ;; esac _bluealsa_complete_options "$1" } complete -F _bluealsad bluealsad complete -F _bluealsactl bluealsactl complete -F _bluealsa_aplay bluealsa-aplay complete -F _bluealsa_rfcomm bluealsa-rfcomm complete -F _bluealsa_others hcitop complete -F _bluealsa_others a2dpconf