PR #23288 opened by add-uos-ffmpeg URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23288 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23288.patch
Add type-specific completion for codec options in ffmpeg, ffplay, and ffprobe. Filter codec suggestions by media type (audio/video/subtitle/data). >From a02d050a1380a48a175b6d4724082977a26f8e4e Mon Sep 17 00:00:00 2001 From: zhanghongyuan <[email protected]> Date: Sun, 31 May 2026 23:56:53 +0800 Subject: [PATCH] fftools/bash-completion: add type-filtered codec completion Add type-specific completion for codec options in ffmpeg, ffplay, and ffprobe. Filter codec suggestions by media type (audio/video/subtitle/data). --- Makefile | 17 ++- configure | 5 + fftools/bash-completion/common | 245 ++++++++++++++++++++++++++++++++ fftools/bash-completion/ffmpeg | 91 ++++++++++++ fftools/bash-completion/ffplay | 73 ++++++++++ fftools/bash-completion/ffprobe | 71 +++++++++ 6 files changed, 500 insertions(+), 2 deletions(-) create mode 100644 fftools/bash-completion/common create mode 100644 fftools/bash-completion/ffmpeg create mode 100644 fftools/bash-completion/ffplay create mode 100644 fftools/bash-completion/ffprobe diff --git a/Makefile b/Makefile index f296e87ed4..2a800c1370 100644 --- a/Makefile +++ b/Makefile @@ -167,15 +167,28 @@ libavutil/ffversion.h .version: # force version.sh to run whenever version might have changed -include .version -install: install-libs install-headers +install: install-libs install-headers install-bash-completion install-libs: install-libs-yes +install-bash-completion: + $(Q)mkdir -p "$(BASH_COMPLETION_DIR)" + $(INSTALL) -m 644 $(SRC_PATH)/fftools/bash-completion/ffmpeg "$(BASH_COMPLETION_DIR)/ffmpeg" + $(INSTALL) -m 644 $(SRC_PATH)/fftools/bash-completion/ffprobe "$(BASH_COMPLETION_DIR)/ffprobe" + $(INSTALL) -m 644 $(SRC_PATH)/fftools/bash-completion/ffplay "$(BASH_COMPLETION_DIR)/ffplay" + $(INSTALL) -m 644 $(SRC_PATH)/fftools/bash-completion/common "$(BASH_COMPLETION_DIR)/common" + install-data: $(DATA_FILES) $(Q)mkdir -p "$(DATADIR)" $(INSTALL) -m 644 $(DATA_FILES) "$(DATADIR)" -uninstall: uninstall-data uninstall-headers uninstall-libs uninstall-pkgconfig +uninstall: uninstall-data uninstall-headers uninstall-libs uninstall-pkgconfig uninstall-bash-completion + +uninstall-bash-completion: + $(RM) "$(BASH_COMPLETION_DIR)/ffmpeg" + $(RM) "$(BASH_COMPLETION_DIR)/ffprobe" + $(RM) "$(BASH_COMPLETION_DIR)/common" + $(RM) "$(BASH_COMPLETION_DIR)/ffplay" uninstall-data: $(RM) -r "$(DATADIR)" diff --git a/configure b/configure index b0923e4789..677e6260f1 100755 --- a/configure +++ b/configure @@ -89,6 +89,7 @@ Standard options: --incdir=DIR install includes in DIR [PREFIX/include] --mandir=DIR install man page in DIR [PREFIX/share/man] --pkgconfigdir=DIR install pkg-config files in DIR [LIBDIR/pkgconfig] + --bashcompletiondir=DIR install bash completion files in DIR [PREFIX/share/bash-completion/completions] --enable-rpath use rpath to allow installing libraries in paths not part of the dynamic linker search path use rpath when linking programs (USE WITH CARE) @@ -2871,6 +2872,7 @@ CMDLINE_SELECT=" " PATHS_LIST=" + bashcompletiondir bindir datadir docdir @@ -4408,6 +4410,7 @@ docdir_default='${prefix}/share/doc/ffmpeg' incdir_default='${prefix}/include' libdir_default='${prefix}/lib' mandir_default='${prefix}/share/man' +bashcompletiondir_default='${prefix}/share/bash-completion/completions' # toolchain ar_default="ar" @@ -6524,6 +6527,7 @@ test_cpp_condition stdlib.h "defined(__PIC__) || defined(__pic__) || defined(PIC set_default libdir : ${shlibdir_default:="$libdir"} : ${pkgconfigdir_default:="$libdir/pkgconfig"} +set_default bashcompletiondir set_default $PATHS_LIST set_default nm @@ -8622,6 +8626,7 @@ DATADIR=\$(DESTDIR)$datadir DOCDIR=\$(DESTDIR)$docdir MANDIR=\$(DESTDIR)$mandir PKGCONFIGDIR=\$(DESTDIR)$pkgconfigdir +BASH_COMPLETION_DIR=\$(DESTDIR)$bashcompletiondir INSTALL_NAME_DIR=$install_name_dir SRC_PATH=$source_path SRC_LINK=$source_link diff --git a/fftools/bash-completion/common b/fftools/bash-completion/common new file mode 100644 index 0000000000..c083d44eff --- /dev/null +++ b/fftools/bash-completion/common @@ -0,0 +1,245 @@ +# Bash version check for declare -g (Bash 4.2+) +# Fallback for older Bash versions (e.g., CentOS 6 uses Bash 4.1) +_bash_version_ok=1 +if [[ ${BASH_VERSION%%.*} -lt 4 ]]; then + _bash_version_ok=0 +elif [[ ${BASH_VERSION%%.*} -eq 4 ]]; then + # Extract minor version number + _bash_minor="${BASH_VERSION#*.}" + _bash_minor="${_bash_minor%%.*}" + if [[ $_bash_minor -lt 2 ]]; then + _bash_version_ok=0 + fi +fi + +if [[ $_bash_version_ok -eq 0 ]]; then + declare -A _FF_CACHE + declare _FF_CACHE_TIME=0 + declare _FF_CACHE_TTL=3600 +else + declare -gA _FF_CACHE + declare -g _FF_CACHE_TIME=0 + declare -g _FF_CACHE_TTL=3600 +fi +unset _bash_version_ok _bash_minor + +_ff_cached_get() { + local key="$1" + local cmd="$2" + local current_time + current_time=$(date +%s) + + if (( current_time - _FF_CACHE_TIME > _FF_CACHE_TTL )); then + _FF_CACHE=() + _FF_CACHE_TIME=$current_time + fi + + if [[ -z "${_FF_CACHE[$key]}" ]]; then + _FF_CACHE[$key]=$(eval "$cmd" 2>/dev/null) + fi + echo "${_FF_CACHE[$key]}" +} + +# Generic encoder filtering with multi-type support +_ff_get_encoders_by_type() { + local types="$1" + local cache_key + local awk_pattern + local valid_types="" + + if [[ -z "$types" ]]; then + cache_key="encoders_all" + awk_pattern='^ [AVSD]' + else + local i + for ((i=0; i<${#types}; i++)); do + local char="${types:$i:1}" + if [[ "$char" =~ [AVSD] ]]; then + valid_types+="$char" + fi + done + + if [[ -z "$valid_types" ]]; then + return + fi + + cache_key="encoders_${valid_types}" + local type_list="" + local i + for ((i=0; i<${#valid_types}; i++)); do + type_list+="${valid_types:$i:1}|" + done + type_list="${type_list%|}" + awk_pattern="^ (${type_list})" + fi + + _ff_cached_get "$cache_key" "ffmpeg -hide_banner -encoders 2>/dev/null | awk '/$awk_pattern/ && !/=/{print \$2}' | grep -v '^\$' | sort -u" +} + +_ff_get_audio_encoders() { + _ff_get_encoders_by_type "A" +} + +_ff_get_video_encoders() { + _ff_get_encoders_by_type "V" +} + +_ff_get_subtitle_encoders() { + _ff_get_encoders_by_type "S" +} + +_ff_get_data_encoders() { + _ff_get_encoders_by_type "D" +} + +_ff_get_av_encoders() { + _ff_get_encoders_by_type "AV" +} + +_ff_get_encoders() { + _ff_cached_get "encoders" "ffmpeg -hide_banner -encoders 2>/dev/null | awk '/^ [AVS]/ && !/=/{print \$2}' | grep -v '^\$' | sort -u" +} + +# Generic decoder filtering with multi-type support +_ff_get_decoders_by_type() { + local types="$1" + local cache_key + local awk_pattern + local valid_types="" + + if [[ -z "$types" ]]; then + cache_key="decoders_all" + awk_pattern='^ [AVSD]' + else + local i + for ((i=0; i<${#types}; i++)); do + local char="${types:$i:1}" + if [[ "$char" =~ [AVSD] ]]; then + valid_types+="$char" + fi + done + + if [[ -z "$valid_types" ]]; then + return + fi + + cache_key="decoders_${valid_types}" + local type_list="" + local i + for ((i=0; i<${#valid_types}; i++)); do + type_list+="${valid_types:$i:1}|" + done + type_list="${type_list%|}" + awk_pattern="^ (${type_list})" + fi + + _ff_cached_get "$cache_key" "ffmpeg -hide_banner -decoders 2>/dev/null | awk '/$awk_pattern/ && !/=/{print \$2}' | grep -v '^\$' | sort -u" +} + +_ff_get_audio_decoders() { + _ff_get_decoders_by_type "A" +} + +_ff_get_video_decoders() { + _ff_get_decoders_by_type "V" +} + +_ff_get_subtitle_decoders() { + _ff_get_decoders_by_type "S" +} + +_ff_get_data_decoders() { + _ff_get_decoders_by_type "D" +} + +_ff_get_av_decoders() { + _ff_get_decoders_by_type "AV" +} + +_ff_get_decoders() { + _ff_cached_get "decoders" "ffmpeg -hide_banner -decoders 2>/dev/null | awk '/^ [AVS]/ && !/=/{print \$2}' | grep -v '^\$' | sort -u" +} + +_ff_get_filters() { + _ff_cached_get "filters" "ffmpeg -hide_banner -filters 2>/dev/null | awk '/^ [^. ]/{print \$2}' | sort -u" +} + +_ff_get_muxers() { + _ff_cached_get "muxers" "ffmpeg -hide_banner -muxers 2>/dev/null | awk '/^ E/{print \$2}' | sort -u" +} + +_ff_get_demuxers() { + _ff_cached_get "demuxers" "ffmpeg -hide_banner -demuxers 2>/dev/null | awk '/^ D /{print \$2}' | sort -u" +} + +_ff_get_pix_fmts() { + _ff_cached_get "pix_fmts" "ffmpeg -hide_banner -pix_fmts 2>/dev/null | awk '/^[A-Z.]{5} [a-z]/{print \$2}' | sort -u" +} + +_ff_get_sample_fmts() { + _ff_cached_get "sample_fmts" "ffmpeg -hide_banner -sample_fmts 2>/dev/null | awk 'NR>1{print \$1}' | sort -u" +} + +_ff_get_protocols() { + _ff_cached_get "protocols" "ffmpeg -hide_banner -protocols 2>/dev/null | awk '/^ [a-z]/{print \$1}' | sort -u" +} + +_ff_get_bsfs() { + _ff_cached_get "bsfs" "ffmpeg -hide_banner -bsfs 2>/dev/null | awk '/^[a-z]/{print \$1}' | sort -u" +} + +_ff_get_codecs() { + _ff_cached_get "codecs" "ffmpeg -hide_banner -codecs 2>/dev/null | awk '/^ [AVS]/{print \$2}' | grep -v '^\$' | sort -u" +} + +_ff_get_formats() { + _ff_cached_get "formats" "ffmpeg -hide_banner -formats 2>/dev/null | awk '/^ [DE ] [^=]/{print \$2}' | sort -u" +} + +_ff_get_hwaccels() { + _ff_cached_get "hwaccels" "ffmpeg -hide_banner -hwaccels 2>/dev/null | awk '/^[a-z]/{print \$1}' | sort -u" +} + +_ff_help_get_values() { + local cur="$1" + + case "${cur}" in + decoder=*) + compopt -o nospace + COMPREPLY=( $(compgen -W "$(_ff_get_decoders)" -- "${cur#decoder=}") ) + ;; + encoder=*) + compopt -o nospace + COMPREPLY=( $(compgen -W "$(_ff_get_encoders)" -- "${cur#encoder=}") ) + ;; + demuxer=*) + compopt -o nospace + COMPREPLY=( $(compgen -W "$(_ff_get_demuxers)" -- "${cur#demuxer=}") ) + ;; + muxer=*) + compopt -o nospace + COMPREPLY=( $(compgen -W "$(_ff_get_muxers)" -- "${cur#muxer=}") ) + ;; + filter=*) + compopt -o nospace + COMPREPLY=( $(compgen -W "$(_ff_get_filters)" -- "${cur#filter=}") ) + ;; + bsf=*) + compopt -o nospace + COMPREPLY=( $(compgen -W "$(_ff_get_bsfs)" -- "${cur#bsf=}") ) + ;; + protocol=*) + compopt -o nospace + COMPREPLY=( $(compgen -W "$(_ff_get_protocols)" -- "${cur#protocol=}") ) + ;; + *) + compopt -o nospace + COMPREPLY=( $(compgen -W "long full decoder= encoder= demuxer= muxer= filter= bsf= protocol=" -- "$cur") ) + ;; + esac +} + +_ff_complete_loglevel() { + local cur="$1" + COMPREPLY=( $(compgen -W "quiet panic fatal error warning info verbose debug trace" -- "$cur") ) +} diff --git a/fftools/bash-completion/ffmpeg b/fftools/bash-completion/ffmpeg new file mode 100644 index 0000000000..c10a607d6a --- /dev/null +++ b/fftools/bash-completion/ffmpeg @@ -0,0 +1,91 @@ +if [[ -f "${BASH_SOURCE[0]%%/ffmpeg}/common" ]]; then + source "${BASH_SOURCE[0]%%/ffmpeg}/common" +fi + +_ffmpeg_get_options() { + ffmpeg -hide_banner -h full 2>/dev/null | awk '/^-/{print $1}' | sort -u +} + +_ffmpeg_completion() { + local cur prev words cword + _init_completion -s || return + + case "${prev}" in + -h|-\?|-help|--help) + _ff_help_get_values "$cur" + return 0 + ;; + -c:v) + COMPREPLY=( $(compgen -W "copy $(_ff_get_video_encoders) $(_ff_get_video_decoders)" -- "$cur") ) + return 0 + ;; + -c:a) + COMPREPLY=( $(compgen -W "copy $(_ff_get_audio_encoders) $(_ff_get_audio_decoders)" -- "$cur") ) + return 0 + ;; + -c:s) + COMPREPLY=( $(compgen -W "copy $(_ff_get_subtitle_encoders) $(_ff_get_subtitle_decoders)" -- "$cur") ) + return 0 + ;; + -c|-codec|-vcodec|-acodec|-scodec) + COMPREPLY=( $(compgen -W "$(_ff_get_encoders)" -- "$cur") ) + return 0 + ;; + -c:v:*|-c:a:*|-c:s:*) + COMPREPLY=( $(compgen -W "copy $(_ff_get_encoders) $(_ff_get_decoders)" -- "$cur") ) + return 0 + ;; + -c:av) + COMPREPLY=( $(compgen -W "copy $(_ff_get_av_encoders) $(_ff_get_av_decoders)" -- "$cur") ) + return 0 + ;; + -c:avs) + COMPREPLY=( $(compgen -W "copy $(_ff_get_encoders_by_type 'AVS') $(_ff_get_decoders_by_type 'AVS')" -- "$cur") ) + return 0 + ;; + -f|-format) + COMPREPLY=( $(compgen -W "$(_ff_get_formats)" -- "$cur") ) + return 0 + ;; + -vf|-af|-filter|-filter_complex) + COMPREPLY=( $(compgen -W "$(_ff_get_filters)" -- "$cur") ) + return 0 + ;; + -pix_fmt) + COMPREPLY=( $(compgen -W "$(_ff_get_pix_fmts)" -- "$cur") ) + return 0 + ;; + -sample_fmt) + COMPREPLY=( $(compgen -W "$(_ff_get_sample_fmts)" -- "$cur") ) + return 0 + ;; + -bsf|-vbsf|-absf) + COMPREPLY=( $(compgen -W "$(_ff_get_bsfs)" -- "$cur") ) + return 0 + ;; + -hwaccel) + COMPREPLY=( $(compgen -W "auto none $(_ff_get_hwaccels)" -- "$cur") ) + return 0 + ;; + -i) + _filedir + return 0 + ;; + -ss|-to|-t|-timestamp|-frames|-vframes|-aframes|-sframes|-r|-s|-b|-b:v|-b:a|-ab|-ar|-ac|-crf|-preset|-tune|-profile|-level|-qscale|-qmin|-qmax|-map|-map_metadata|-map_chapters|-metadata|-x265-params|-x264-params) + return 0 + ;; + -loglevel|-v) + _ff_complete_loglevel "$cur" + return 0 + ;; + esac + + if [[ "$cur" == -* ]]; then + COMPREPLY=( $(compgen -W "$(_ffmpeg_get_options)" -- "$cur") ) + return 0 + fi + + _filedir +} + +complete -F _ffmpeg_completion ffmpeg diff --git a/fftools/bash-completion/ffplay b/fftools/bash-completion/ffplay new file mode 100644 index 0000000000..c345bf6b07 --- /dev/null +++ b/fftools/bash-completion/ffplay @@ -0,0 +1,73 @@ +if [[ -f "${BASH_SOURCE[0]%%/ffplay}/common" ]]; then + source "${BASH_SOURCE[0]%%/ffplay}/common" +fi + +_ffplay_get_options() { + ffplay -hide_banner -h 2>/dev/null | awk '/^-/{print $1}' | sort -u +} + +_ff_complete_showmode() { + local cur="$1" + COMPREPLY=( $(compgen -W "video waves rdft" -- "$cur") ) +} + +_ff_complete_sync() { + local cur="$1" + COMPREPLY=( $(compgen -W "audio video ext" -- "$cur") ) +} + +_ffplay_completion() { + local cur prev words cword + _init_completion -s || return + + case "${prev}" in + -h|-\?|-help|--help) + _ff_help_get_values "$cur" + return 0 + ;; + -acodec) + COMPREPLY=( $(compgen -W "$(_ff_get_audio_decoders)" -- "$cur") ) + return 0 + ;; + -vcodec) + COMPREPLY=( $(compgen -W "$(_ff_get_video_decoders)" -- "$cur") ) + return 0 + ;; + -scodec) + COMPREPLY=( $(compgen -W "$(_ff_get_subtitle_decoders)" -- "$cur") ) + return 0 + ;; + -i) + _filedir + return 0 + ;; + -vf|-af|-filter|-filter_complex) + COMPREPLY=( $(compgen -W "$(_ff_get_filters)" -- "$cur") ) + return 0 + ;; + -ss|-to|-t|-timestamp|-frames|-vframes|-aframes|-window_title|-x|-y|-width|-height|-volume|-mute|-loop|-framedrop|-reorder_queue_size|-rdftspeed) + return 0 + ;; + -sync) + _ff_complete_sync "$cur" + return 0 + ;; + -showmode) + _ff_complete_showmode "$cur" + return 0 + ;; + -loglevel|-v) + _ff_complete_loglevel "$cur" + return 0 + ;; + esac + + if [[ "$cur" == -* ]]; then + COMPREPLY=( $(compgen -W "$(_ffplay_get_options)" -- "$cur") ) + return 0 + fi + + _filedir +} + +complete -F _ffplay_completion ffplay diff --git a/fftools/bash-completion/ffprobe b/fftools/bash-completion/ffprobe new file mode 100644 index 0000000000..89c31baf98 --- /dev/null +++ b/fftools/bash-completion/ffprobe @@ -0,0 +1,71 @@ +if [[ -f "${BASH_SOURCE[0]%%/ffprobe}/common" ]]; then + source "${BASH_SOURCE[0]%%/ffprobe}/common" +fi + +_ffprobe_get_options() { + ffprobe -hide_banner -h 2>/dev/null | awk '/^-/{print $1}' | sort -u +} + +_ffprobe_completion() { + local cur prev words cword + _init_completion -s || return + + case "${prev}" in + -h|-\?|-help|--help) + _ff_help_get_values "$cur" + return 0 + ;; + -c:a) + COMPREPLY=( $(compgen -W "$(_ff_get_audio_decoders)" -- "$cur") ) + return 0 + ;; + -c:v) + COMPREPLY=( $(compgen -W "$(_ff_get_video_decoders)" -- "$cur") ) + return 0 + ;; + -c:s) + COMPREPLY=( $(compgen -W "$(_ff_get_subtitle_decoders)" -- "$cur") ) + return 0 + ;; + -c:d) + COMPREPLY=( $(compgen -W "$(_ff_get_data_decoders)" -- "$cur") ) + return 0 + ;; + -output_format|-print_format|-of) + COMPREPLY=( $(compgen -W "default compact csv flat ini json xml" -- "$cur") ) + return 0 + ;; + -select_streams) + COMPREPLY=( $(compgen -W "v a s d t" -- "$cur") ) + return 0 + ;; + -f) + COMPREPLY=( $(compgen -W "$(_ff_get_formats)" -- "$cur") ) + return 0 + ;; + -i|-o) + _filedir + return 0 + ;; + -loglevel|-v) + _ff_complete_loglevel "$cur" + return 0 + ;; + -show_entries) + COMPREPLY=( $(compgen -W "format stream packet frame program chapter streams packets frames programs chapters" -- "$cur") ) + return 0 + ;; + -read_intervals) + return 0 + ;; + esac + + if [[ "$cur" == -* ]]; then + COMPREPLY=( $(compgen -W "$(_ffprobe_get_options)" -- "$cur") ) + return 0 + fi + + _filedir +} + +complete -F _ffprobe_completion ffprobe -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
