diff --git a/ble.pp b/ble.pp index 817a1f21..376b665f 100644 --- a/ble.pp +++ b/ble.pp @@ -684,30 +684,35 @@ function ble/.check-environment { fi # src/util で awk を使う +_ble_bin_awk_type= function ble/bin/awk/.instantiate { local path q=\' Q="'\''" if [[ $OSTYPE == solaris* ]] && type /usr/xpg4/bin/awk >/dev/null; then # Solaris の既定の awk は全然駄目なので /usr/xpg4 以下の awk を使う。 + _ble_bin_awk_type=xpg4 function ble/bin/awk { /usr/xpg4/bin/awk -v AWKTYPE=xpg4 "$@"; } elif ble/util/assign path "builtin type -P -- nawk 2>/dev/null" && [[ $path ]]; then + _ble_bin_awk_type=nawk builtin eval "function ble/bin/awk { '${path//$q/$Q}' -v AWKTYPE=nawk \"\$@\"; }" elif ble/util/assign path "builtin type -P -- mawk 2>/dev/null" && [[ $path ]]; then + _ble_bin_awk_type=mawk builtin eval "function ble/bin/awk { '${path//$q/$Q}' -v AWKTYPE=mawk \"\$@\"; }" elif ble/util/assign path "builtin type -P -- gawk 2>/dev/null" && [[ $path ]]; then + _ble_bin_awk_type=gawk builtin eval "function ble/bin/awk { '${path//$q/$Q}' -v AWKTYPE=gawk \"\$@\"; }" elif ble/util/assign path "builtin type -P -- awk 2>/dev/null" && [[ $path ]]; then - local version type + local version ble/util/assign version '"$path" --version 2>&1' if [[ $version == *'GNU Awk'* ]]; then - type=gawk + _ble_bin_awk_type=gawk elif [[ $version == *mawk* ]]; then - type=mawk + _ble_bin_awk_type=mawk elif [[ $version == 'awk version '[12][0-9][0-9][0-9][01][0-9][0-3][0-9] ]]; then - type=nawk + _ble_bin_awk_type=nawk else - type=unknown + _ble_bin_awk_type=unknown fi - builtin eval "function ble/bin/awk { '${path//$q/$Q}' -v AWKTYPE=$type \"\$@\"; }" + builtin eval "function ble/bin/awk { '${path//$q/$Q}' -v AWKTYPE=$_ble_bin_awk_type \"\$@\"; }" else return 1 fi diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 6a7b8d98..bda3abbb 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -202,6 +202,7 @@ - util (`ble/util/readfile`): fix a bug of always exiting with 1 in `bash <= 3.2` (reported by laoshaw) `#D1678` 61705bf - trace: fix wrong positioning of the ellipses on overflow `#D1684` b90ac78 - complete: do not generate keywords for quoted command names `#D1691` 60d244f +- menu (menu-style:align): fix the failure of delaying `ble/canvas/trace` on items (motivated by banoris) `#D1710` acc9661 - complete: fix empty completions with `FIGNORE` (reported by seanfarley) `#D1711` 144ea5d - main: fix the message of owner errors of cache directories (reported by zim0369) `#D1712` b547a41 - util (`ble/string#escape-for-bash-specialchars`): fix escaping of TAB `#D1713` 0000000 @@ -231,6 +232,7 @@ - prompt: use `${PS1@P}` when the prompt contains only safe prompt sequences `#D1617` 8b5da08 - prompt: fix not properly set `$?` in `${PS1@P}` evaluation (reported by nihilismus) `#D1644` 521aff9 - decode: cache `inputrc` translations `#D1652` 994e2a5 +- complete: use `awk` for batch `quote-insert` (motivated by banoris) `#D1714` 0000000 ## Compatibility diff --git a/lib/core-complete.sh b/lib/core-complete.sh index 00218696..a2e0cff0 100644 --- a/lib/core-complete.sh +++ b/lib/core-complete.sh @@ -1157,82 +1157,358 @@ function ble/complete/string#escape-for-completion-context { esac } -function ble/complete/action/util/complete.addtail { +function ble/complete/action/complete.addtail { suffix=$suffix$1 } -function ble/complete/action/util/complete.mark-directory { +function ble/complete/action/complete.mark-directory { [[ :$comp_type: == *:markdir:* && $CAND != */ ]] && [[ :$comp_type: == *:marksymdir:* || ! -h $CAND ]] && - ble/complete/action/util/complete.addtail / + ble/complete/action/complete.addtail / } -function ble/complete/action/util/complete.close-quotation { +function ble/complete/action/complete.close-quotation { case $comps_flags in - (*[SE]*) ble/complete/action/util/complete.addtail \' ;; - (*[DI]*) ble/complete/action/util/complete.addtail \" ;; + (*[SE]*) ble/complete/action/complete.addtail \' ;; + (*[DI]*) ble/complete/action/complete.addtail \" ;; esac } -## @fn ble/complete/action/util/quote-insert type -function ble/complete/action/util/quote-insert { - local escape_flags=c - if [[ $1 == command ]]; then - escape_flags= - elif [[ $1 == progcomp ]]; then +## @fn ble/complete/action/quote-insert.initialize action +## @var[out] ${_ble_complete_quote_insert_varnames[@]} +## +## @fn ble/complete/action/quote-insert action +## @var[ref] INSERT +## @var[in] ${_ble_complete_quote_insert_varnames[@]} +## +## Note: quote-insert を呼び出す前に予め quote-insert.initialize を呼び出して +## quote_... 変数を初期化しておく必要があります。 +## +## Example: +## +## local "${_ble_complete_quote_insert_varnames[@]/%/=}" # WA #D1570 safe +## ble/complete/action/quote-insert.initialize "$action" +## for INSERT; do +## ble/complete/action/quote-insert "$action" +## : do something with INSERT +## done +## + +_ble_complete_quote_insert_varnames=( + quote_action + quote_escape_flags + quote_cont_cutbackslash + quote_paramx_comps + quote_trav_prefix + quote_fixed_comps + quote_fixed_compv + quote_fixed_comps_len + quote_fixed_compv_len) + +function ble/complete/action/quote-insert.initialize { + quote_action=$1 + + quote_escape_flags=c + if [[ $quote_action == command ]]; then + quote_escape_flags= + elif [[ $quote_action == progcomp ]]; then # #D1362 Bash は "compopt -o filenames" が指定されている時、 # '~' で始まる補完候補と同名のファイルがある時にのみチルダをクォートする。 - [[ $INSERT == '~'* && ! ( $DATA == *:filenames:* && -e $INSERT ) ]] && - escape_flags=T$escape_flags + # [[ $CAND == '~'* && ! ( $comp_opts == *:filenames:* && -e $CAND ) ]] && + # quote_escape_flags=T$quote_escape_flags # #D1434 = 及び : は filenames がついていない限りは quote しない事にする。 # bash-complete が unquoted =, : を生成する可能性があるので。 - [[ $DATA != *:filenames:* ]] && - escape_flags=${escape_flags//c} + [[ $comp_opts != *:filenames:* ]] && + quote_escape_flags=${quote_escape_flags//c} + fi + [[ $comps_fixed ]] && quote_escape_flags=b$quote_escape_flags + + # 孤立 backslash が前置している時は二重クォートを防ぐ為に削除 + quote_cont_cutbackslash= + [[ $comps_flags == *B* && $COMPS == *'\' ]] && + quote_cont_cutbackslash=1 + + # 直前にパラメータ展開があればエスケープ + quote_paramx_comps=$COMPS + if [[ $comps_flags == *p* ]]; then + # Note: 安全策 (本来 comps_flags に p がある時点で '\' では終わらない筈) + [[ $comps_flags == *B* && $quote_paramx_comps == *'\' ]] && + quote_paramx_comps=${quote_paramx_comps%'\'} + + case $comps_flags in + (*[DI]*) + if [[ $COMPS =~ $rex_raw_paramx ]]; then + local rematch1=${BASH_REMATCH[1]} + quote_paramx_comps=$rematch1'${'${COMPS:${#rematch1}+1}'}' + else + # Note: 安全策 (本来上で一致する筈) + quote_paramx_comps=$quote_paramx_comps'""' + fi ;; + (*) + quote_paramx_comps=$quote_paramx_comps'\' ;; + esac + fi + + # 遡って書き換えた時に文脈を復元 + quote_trav_prefix= + case $comps_flags in + (*S*) quote_trav_prefix=\' ;; + (*E*) quote_trav_prefix=\$\' ;; + (*D*) quote_trav_prefix=\" ;; + (*I*) quote_trav_prefix=\$\" ;; + esac + + # 遡って書き換える時に comps_fixed には注意する。 + quote_fixed_comps= + quote_fixed_compv= + quote_fixed_comps_len= + quote_fixed_compv_len= + if [[ $comps_fixed ]]; then + quote_fixed_compv=${comps_fixed#*:} + quote_fixed_compv_len=${#quote_fixed_compv} + quote_fixed_comps_len=${comps_fixed%%:*} + quote_fixed_comps=${COMPS::quote_fixed_comps_len} + fi +} + +function ble/complete/action/quote-insert { + if [[ ! $quote_action ]]; then + local "${_ble_complete_quote_insert_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/action/quote-insert.initialize "${1:-plain}" + fi + + local escape_flags=$quote_escape_flags + if [[ $quote_action == progcomp ]]; then + [[ $comp_opts == *:noquote:* ]] && return 0 + + # bash-completion には compopt -o nospace として、 + # 自分でスペースを付加する補完関数がある。この時クォートすると問題。 + [[ $comp_opts == *:nospace:* && $CAND == *' ' && ! -f $CAND ]] && return 0 + + # #D1362 Bash は "compopt -o filenames" が指定されていてかつ + # '~' で始まる補完候補と同名のファイルがある時にのみチルダをクォートする。 + [[ $CAND == '~'* && ! ( $comp_opts == *:filenames:* && -e $CAND ) ]] && + escape_flags=T$escape_flags fi if [[ $comps_flags == *v* && $CAND == "$COMPV"* ]]; then - local ins=${CAND:${#COMPV}} ret - - # 単語内の文脈に応じたエスケープ - ble/complete/string#escape-for-completion-context "$ins" "$escape_flags"; ins=$ret - - # 直前にパラメータ展開があればエスケープ - if [[ $comps_flags == *p* && $ins == [a-zA-Z_0-9]* ]]; then - case $comps_flags in - (*[DI]*) - if [[ $COMPS =~ $rex_raw_paramx ]]; then - local rematch1=${BASH_REMATCH[1]} - INSERT=$rematch1'${'${COMPS:${#rematch1}+1}'}'$ins - return 0 - else - ins='""'$ins - fi ;; - (*) ins='\'$ins ;; - esac + local ins ret + ble/complete/string#escape-for-completion-context "${CAND:${#COMPV}}" "$escape_flags"; ins=$ret + if [[ $comps_flags == *p* && $ins == [_a-zA-Z0-9]* ]]; then + INSERT=$quote_paramx_comps$ins + else + [[ $quote_cont_cutbackslash ]] && ins=${ins#'\'} + INSERT=$COMPS$ins; fi + elif [[ $quote_fixed_comps && $CAND == "$quote_fixed_compv"* ]]; then + local ret; ble/complete/string#escape-for-completion-context "${CAND:quote_fixed_compv_len}" "$escape_flags" + INSERT=$quote_fixed_comps$quote_trav_prefix$ret + else + local ret; ble/complete/string#escape-for-completion-context "$CAND" "$escape_flags" + INSERT=$quote_trav_prefix$ret + fi +} + +function ble/complete/action/quote-insert.batch/awk { + local q=\' + local -x comp_opts=$comp_opts + local -x comps=$COMPS + local -x compv=$COMPV + local -x comps_flags=$comps_flags + local -x quote_action=$quote_action + local -x quote_escape_flags=$quote_escape_flags + local -x quote_paramx_comps=$quote_paramx_comps + local -x quote_cont_cutbackslash=$quote_cont_cutbackslash + local -x quote_trav_prefix=$quote_trav_prefix + local -x quote_fixed_comps=$quote_fixed_comps + local -x quote_fixed_compv=$quote_fixed_compv + "$quote_batch_awk" -v quote_batch_nulsep="$quote_batch_nulsep" -v q="$q" ' + function exists(filename) { return substr($0, 1, 1) == "1"; } + function is_file(filename) { return substr($0, 2, 1) == "1"; } + + function initialize(_, flags, comp_opts, tmp) { + IS_XPG4 = AWKTYPE == "xpg4"; + REP_SL = "\\"; + if (IS_XPG4) REP_SL = "\\\\"; + + REP_DBL_SL = "\\\\"; # gawk, nawk + sub(/.*/, REP_DBL_SL, tmp); + if (tmp == "\\") REP_DBL_SL = "\\\\\\\\"; # mawk, xpg4 + + Q = q "\\" q q; + + DELIM = 10; + if (quote_batch_nulsep != "") { + RS = "\0"; + DELIM = 0; + } + + quote_action = ENVIRON["quote_action"]; + + comps = ENVIRON["comps"]; + compv = ENVIRON["compv"]; + compv_len = length(compv); + + comps_flags = ENVIRON["comps_flags"]; + escape_type = 0; + if (comps_flags ~ /S/) + escape_type = 1; + else if (comps_flags ~ /E/) + escape_type = 2; + else if (comps_flags ~ /[DI]/) + escape_type = 3; + else + escape_type = 4; + comps_v = (comps_flags ~ /v/); + comps_p = (comps_flags ~ /p/); + + comp_opts = ENVIRON["comp_opts"]; + is_noquote = comp_opts ~ /:noquote:/; + is_nospace = comp_opts ~ /:nospace:/; + + flags = ENVIRON["quote_escape_flags"]; + escape_c = (flags ~ /c/); + escape_b = (flags ~ /b/); + escape_tilde_always = 1; + escape_tilde_exists = 0; + if (quote_action == "progcomp") { + escape_tilde_always = 0; + escape_tilde_exists = (comp_opts ~ /:filenames:/); + } + + quote_cont_cutbackslash = ENVIRON["quote_cont_cutbackslash"] != ""; + quote_paramx_comps = ENVIRON["quote_paramx_comps"]; + quote_trav_prefix = ENVIRON["quote_trav_prefix"]; + quote_fixed_comps = ENVIRON["quote_fixed_comps"]; + quote_fixed_compv = ENVIRON["quote_fixed_compv"]; + quote_fixed_comps_len = length(quote_fixed_comps); + quote_fixed_compv_len = length(quote_fixed_compv); + } + BEGIN { initialize(); } + + function escape_for_completion_context(text) { + if (escape_type == 1) { + # single quote + gsub(/'$q'/, Q, text); + } else if (escape_type == 2) { + # escape string + if (text ~ /[\\'$q'\a\b\t\n\v\f\r\033]/) { + gsub(/\\/ , REP_DBL_SL, text); + gsub(/'$q'/, REP_SL q , text); + gsub(/\007/, REP_SL "a", text); + gsub(/\010/, REP_SL "b", text); + gsub(/\011/, REP_SL "t", text); + gsub(/\012/, REP_SL "n", text); + gsub(/\013/, REP_SL "v", text); + gsub(/\014/, REP_SL "f", text); + gsub(/\015/, REP_SL "r", text); + gsub(/\033/, REP_SL "e", text); + } + } else if (escape_type == 3) { + # double quote + gsub(/[\\"$`]/, "\\\\&", text); # Note: All awks behaves the same for "\\\\&" + } else if (escape_type == 4) { + # bash specialchars + gsub(/[]\ "'$q'`$|&;<>()!^*?[]/, "\\\\&", text); + if (escape_c) gsub(/[=:]/, "\\\\&", text); + if (escape_b) gsub(/[{,}]/, "\\\\&", text); + if (ret ~ /^~/ && (escape_tilde_always || escape_tilde_exists && exists(cand))) + text = "\\" text; + gsub(/\n/, "$" q REP_SL "n" q, text); + gsub(/\t/, "$" q REP_SL "t" q, text); + } + return text; + } - # backslash が前置している時は二重クォートを防ぐ為に削除 - [[ $comps_flags == *B* && $COMPS == *'\' && $ins == '\'* ]] && ins=${ins:1} + function quote_insert(cand) { + # progcomp 特有 + if (quote_action == "progcomp") { + if (is_noquote) return cand; + if (is_nospace && cand ~ / $/ && !is_file(cand)) return cand; + } - INSERT=$COMPS$ins + if (comps_v && substr(cand, 1, compv_len) == compv) { + ins = escape_for_completion_context(substr(cand, compv_len + 1)); + if (comps_p && ins ~ /^[_a-zA-Z0-9]/) { + return quote_paramx_comps ins; + } else { + if (quote_cont_cutbackslash) sub(/^\\/, "", ins); + return comps ins; + } + } else if (quote_fixed_comps_len && substr(cand, 1, quote_fixed_compv_len) == quote_fixed_compv) { + ins = substr(cand, quote_fixed_compv_len + 1); + return quote_fixed_comps quote_trav_prefix escape_for_completion_context(ins); + } else { + return quote_trav_prefix escape_for_completion_context(cand); + } + } + + { + cand = substr($0, 3); + insert = quote_insert(cand); + printf("%s%c", insert, DELIM); + } + ' +} +function ble/complete/action/quote-insert.batch/proc { + local _ble_local_tmpfile; ble/util/assign/.mktmp + + local delim='\n' + [[ $quote_batch_nulsep ]] && delim='\0' + if [[ $quote_action == progcomp ]]; then + local cand file exist + for cand in "${cands[@]}"; do + ((cand_iloop++%bleopt_complete_polling_cycle==0)) && ble/complete/check-cancel && return 148 + f=0 e=0 + [[ -e $cand ]] && e=1 + [[ -f $cand ]] && f=1 + printf "$e$f%s$delim" "$cand" + done else - local ins=$CAND comps_fixed_part= compv_fixed_part= - if [[ $comps_fixed && $CAND == "${comps_fixed#*:}"* ]]; then - comps_fixed_part=${COMPS::${comps_fixed%%:*}} - compv_fixed_part=${comps_fixed#*:} - ins=${CAND:${#compv_fixed_part}} - fi + printf "00%s$delim" "${cands[@]}" + fi >| "$_ble_local_tmpfile" - local ret; ble/complete/string#escape-for-completion-context "$ins" "$escape_flags"; ins=$ret - case $comps_flags in - (*S*) ins=\'$ins ;; - (*E*) ins=\$\'$ins ;; - (*D*) ins=\"$ins ;; - (*I*) ins=\$\"$ins ;; - esac + local fname_cands=$_ble_local_tmpfile + ble/util/conditional-sync \ + 'ble/complete/action/quote-insert.batch/awk < "$fname_cands"' \ + '! ble/complete/check-cancel < /dev/tty' '' progressive-weight + local ext=$? + + ble/util/assign/.rmtmp + return "$ext" +} +## @fn ble/complete/action/quote-insert.batch +## @arr[in] cands +## @arr[out] inserts +function ble/complete/action/quote-insert.batch { + local opts=$1 + + local quote_batch_nulsep= + local quote_batch_awk=ble/bin/awk + if [[ :$opts: != *:newline:* ]]; then + if ((_ble_bash>=40400)); then + if [[ $_ble_bin_awk_type == [mg]awk ]]; then + quote_batch_nulsep=1 + elif ble/bin#has mawk; then + quote_batch_nulsep=1 + quote_batch_awk=mawk + elif ble/bin#has gawk; then + quote_batch_nulsep=1 + quote_batch_awk=gawk + fi + fi + [[ ! $quote_batch_nulsep ]] && + [[ "${cands[*]}" == *$'\n'* ]] && + return 1 + fi - INSERT=$comps_fixed_part$ins + if [[ $quote_batch_nulsep ]]; then + ble/util/assign-array0 inserts ble/complete/action/quote-insert.batch/proc + else + ble/util/assign-array inserts ble/complete/action/quote-insert.batch/proc fi + return $? } -function ble/complete/action/util/requote-final-insert { + +function ble/complete/action/requote-final-insert { if [[ $insert == "$COMPS"* ]]; then [[ $comps_flags == *[SEDI]* ]] && return 0 local comps_prefix=$COMPS @@ -1257,10 +1533,10 @@ function ble/complete/action/util/requote-final-insert { return 0 } -function ble/complete/action/inherit-from { +function ble/complete/action#inherit-from { local dst=$1 src=$2 local member srcfunc dstfunc - for member in initialize complete getg get-desc; do + for member in initialize{,.batch} complete getg get-desc; do srcfunc=ble/complete/action:$src/$member dstfunc=ble/complete/action:$dst/$member ble/is-function "$srcfunc" && builtin eval "function $dstfunc { $srcfunc; }" @@ -1269,31 +1545,39 @@ function ble/complete/action/inherit-from { # action:plain function ble/complete/action:plain/initialize { - ble/complete/action/util/quote-insert + ble/complete/action/quote-insert +} +function ble/complete/action:plain/initialize.batch { + ble/complete/action/quote-insert.batch } function ble/complete/action:plain/complete { - ble/complete/action/util/requote-final-insert + ble/complete/action/requote-final-insert } # action:literal-substr function ble/complete/action:literal-substr/initialize { :; } +function ble/complete/action:literal-substr/initialize.batch { inserts=("${cands[@]}"); } function ble/complete/action:literal-substr/complete { :; } # action:substr (equivalent to plain) function ble/complete/action:substr/initialize { - ble/complete/action/util/quote-insert + ble/complete/action/quote-insert +} +function ble/complete/action:substr/initialize.batch { + ble/complete/action/quote-insert.batch } function ble/complete/action:substr/complete { - ble/complete/action/util/requote-final-insert + ble/complete/action/requote-final-insert } # action:literal-word function ble/complete/action:literal-word/initialize { :; } +function ble/complete/action:literal-word/initialize.batch { inserts=("${cands[@]}"); } function ble/complete/action:literal-word/complete { if [[ $comps_flags == *x* ]]; then - ble/complete/action/util/complete.addtail ',' + ble/complete/action/complete.addtail ',' else - ble/complete/action/util/complete.addtail ' ' + ble/complete/action/complete.addtail ' ' fi } @@ -1302,11 +1586,14 @@ function ble/complete/action:literal-word/complete { # DATA ... 候補の説明として使用する文字列を指定します # function ble/complete/action:word/initialize { - ble/complete/action/util/quote-insert + ble/complete/action/quote-insert +} +function ble/complete/action:word/initialize.batch { + ble/complete/action/quote-insert.batch } function ble/complete/action:word/complete { - ble/complete/action/util/requote-final-insert - ble/complete/action/util/complete.close-quotation + ble/complete/action/requote-final-insert + ble/complete/action/complete.close-quotation ble/complete/action:literal-word/complete } function ble/complete/action:word/get-desc { @@ -1316,13 +1603,16 @@ function ble/complete/action:word/get-desc { # action:file # action:file_rhs (source:argument 内部使用) function ble/complete/action:file/initialize { - ble/complete/action/util/quote-insert + ble/complete/action/quote-insert +} +function ble/complete/action:file/initialize.batch { + ble/complete/action/quote-insert.batch } function ble/complete/action:file/complete { - ble/complete/action/util/requote-final-insert + ble/complete/action/requote-final-insert if [[ -e $CAND || -h $CAND ]]; then if [[ -d $CAND ]]; then - ble/complete/action/util/complete.mark-directory + ble/complete/action/complete.mark-directory else ble/complete/action:word/complete fi @@ -1345,6 +1635,9 @@ function ble/complete/action:file/init-menu-item { function ble/complete/action:file_rhs/initialize { ble/complete/action:file/initialize } +function ble/complete/action:file_rhs/initialize.batch { + ble/complete/action:file/initialize.batch +} function ble/complete/action:file_rhs/complete { CAND=${CAND:${#DATA}} ble/complete/action:file/complete } @@ -1375,21 +1668,19 @@ function ble/complete/action:file/get-desc { # DATA ... compopt 互換のオプションをコロン区切りで指定します # function ble/complete/action:progcomp/initialize { - [[ $DATA == *:noquote:* ]] && return 0 - - # bash-completion には compopt -o nospace として、 - # 自分でスペースを付加する補完関数がある。この時クォートすると問題。 - [[ $DATA == *:nospace:* && $CAND == *' ' && ! -f $CAND ]] && return 0 - - ble/complete/action/util/quote-insert progcomp + ble/complete/action/quote-insert progcomp +} +function ble/complete/action:progcomp/initialize.batch { + ble/complete/action/quote-insert.batch newline } + function ble/complete/action:progcomp/complete { if [[ $DATA == *:filenames:* ]]; then ble/complete/action:file/complete else if [[ $DATA != *:no-mark-directories:* && -d $CAND ]]; then - ble/complete/action/util/requote-final-insert - ble/complete/action/util/complete.mark-directory + ble/complete/action/requote-final-insert + ble/complete/action/complete.mark-directory else ble/complete/action:word/complete fi @@ -1411,11 +1702,14 @@ function ble/complete/action:progcomp/get-desc { # action:command function ble/complete/action:command/initialize { - ble/complete/action/util/quote-insert command + ble/complete/action/quote-insert command +} +function ble/complete/action:command/initialize.batch { + ble/complete/action/quote-insert.batch newline } function ble/complete/action:command/complete { if [[ -d $CAND ]]; then - ble/complete/action/util/complete.mark-directory + ble/complete/action/complete.mark-directory elif ! type "$CAND" &>/dev/null; then # 関数名について縮約されたもので一意確定した時。 # @@ -1444,9 +1738,9 @@ function ble/complete/action:command/init-menu-item { # _ble_attr_ERR を返してしまう。 local type; ble/util/type type "$CAND" ble/syntax/highlight/cmdtype1 "$type" "$CAND" - if [[ $CAND == */ ]] && ((type==_ble_attr_ERR)); then - type=_ble_attr_CMD_FUNCTION - fi + fi + if [[ $CAND == */ ]] && ((type==_ble_attr_ERR)); then + type=_ble_attr_CMD_FUNCTION fi ble/syntax/attr2g "$type" fi @@ -1505,15 +1799,16 @@ function ble/complete/action:command/get-desc { # DATA ... 変数名の文脈を指定します。 # assignment braced word arithmetic の何れかです。 # -function ble/complete/action:variable/initialize { ble/complete/action/util/quote-insert; } +function ble/complete/action:variable/initialize { ble/complete/action/quote-insert; } +function ble/complete/action:variable/initialize.batch { ble/complete/action/quote-insert.batch newline; } function ble/complete/action:variable/complete { case $DATA in (assignment) # var= 等に於いて = を挿入 - ble/complete/action/util/complete.addtail '=' ;; + ble/complete/action/complete.addtail '=' ;; (braced) # ${var 等に於いて } を挿入 - ble/complete/action/util/complete.addtail '}' ;; + ble/complete/action/complete.addtail '}' ;; (word) ble/complete/action:word/complete ;; (arithmetic|nosuffix) ;; # do nothing esac @@ -1643,6 +1938,14 @@ function ble/complete/source/reduce-compv-for-ambiguous-match { COMPS=$comps_prefix$comps } + +_ble_complete_yield_varnames=("${_ble_complete_quote_insert_varnames[@]}") + +## @fn ble/complete/cand/yield.initialize action +function ble/complete/cand/yield.initialize { + ble/complete/action/quote-insert.initialize "$1" +} + ## @fn ble/complete/cand/yield ACTION CAND DATA ## @param[in] ACTION ## @param[in] CAND @@ -1653,9 +1956,7 @@ function ble/complete/source/reduce-compv-for-ambiguous-match { function ble/complete/cand/yield { local ACTION=$1 CAND=$2 DATA=$3 [[ $flag_force_fignore ]] && ! ble/complete/.fignore/filter "$CAND" && return 0 - - [[ $flag_source_filter ]] || - ble/complete/candidates/filter#test "$CAND" || return 0 + [[ $flag_source_filter ]] || ble/complete/candidates/filter#test "$CAND" || return 0 local PREFIX_LEN=0 [[ $CAND == "$COMP_PREFIX"* ]] && PREFIX_LEN=${#COMP_PREFIX} @@ -1670,6 +1971,40 @@ function ble/complete/cand/yield { cand_pack[icand]=$ACTION:${#CAND},${#INSERT},$PREFIX_LEN:$CAND$INSERT$DATA } +## @fn ble/complete/cand/yield.batch action data +## @arr[in] cands +function ble/complete/cand/yield.batch { + local ACTION=$1 DATA=$2 + + local inserts threshold=500 + [[ $OSTYPE == cygwin* || $OSTYPE == msys* ]] && threshold=2000 + if ((${#cands[@]}>=threshold)) && ble/function#try ble/complete/action:"$ACTION"/initialize.batch; then + local i n=${#cands[@]} + for ((i=0;i arr, flag_mandb + local cands flag_mandb= + ble/complete/progcomp/.filter-and-split-compgen cands # compgen (comp_opts, etc) -> cands, flag_mandb - ble/complete/source/test-limit ${#arr[@]} || return 1 + ble/complete/source/test-limit ${#cands[@]} || return 1 - local action=progcomp [[ $comp_opts == *:filenames:* && $COMPV == */* ]] && COMP_PREFIX=${COMPV%/*}/ local old_cand_count=$cand_count + + local action=progcomp "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize "$action" if [[ $flag_mandb ]]; then - local entry fs=$_ble_term_FS has_desc= - for entry in "${arr[@]}"; do + local -a entries; entries=("${cands[@]}") + cands=() + local fs=$_ble_term_FS has_desc= icand=0 entry + for entry in "${entries[@]}"; do ((cand_iloop++%bleopt_complete_polling_cycle==0)) && ble/complete/check-cancel && return 148 if [[ $entry == -*"$fs"*"$fs"*"$fs"* ]]; then local cand=${entry%%"$fs"*} ble/complete/cand/yield mandb "$cand" "$entry" [[ $entry == *"$fs"*"$fs"*"$fs"?* ]] && has_desc=1 else - ble/complete/cand/yield "$action" "$progcomp_prefix$entry" "$comp_opts" + cands[icand++]=$progcomp_prefix$entry fi done [[ $has_desc ]] && bleopt complete_menu_style=desc else - local entry - for entry in "${arr[@]}"; do - ((cand_iloop++%bleopt_complete_polling_cycle==0)) && ble/complete/check-cancel && return 148 - ble/complete/cand/yield "$action" "$progcomp_prefix$entry" "$comp_opts" - done + [[ $progcomp_prefix ]] && cands=("${cands[@]/#/$progcomp_prefix}") # WA #D1570 safe fi + ble/complete/cand/yield.batch "$action" "$comp_opts" # plusdirs の時はディレクトリ名も候補として列挙 # Note: 重複候補や順序については考えていない @@ -3207,13 +3556,16 @@ _ble_complete_option_chars='_!#$%&:;.,^~|\\?\/*a-zA-Z0-9' # DATA ... cmd FS menu_suffix FS insert_suffix FS desc # function ble/complete/action:mandb/initialize { - ble/complete/action/util/quote-insert + ble/complete/action/quote-insert +} +function ble/complete/action:mandb/initialize.batch { + ble/complete/action/quote-insert.batch newline } function ble/complete/action:mandb/complete { - ble/complete/action/util/complete.close-quotation + ble/complete/action/complete.close-quotation local fields ble/string#split fields "$_ble_term_FS" "$DATA" - ble/complete/action/util/complete.addtail "${fields[2]}" + ble/complete/action/complete.addtail "${fields[2]}" } function ble/complete/action:mandb/init-menu-item { local ret; ble/color/face2g argument_option; g=$ret @@ -4257,6 +4609,8 @@ function ble/complete/source:option { # "--" や非オプション引数など、オプション無効化条件をチェック ble/complete/source:option/.is-option-context "${prev_args[@]}" || return 1 + local "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize mandb local entry fs=$_ble_term_FS has_desc= for entry in "${entries[@]}"; do ((cand_iloop++%bleopt_complete_polling_cycle==0)) && @@ -4381,10 +4735,11 @@ function ble/complete/source:argument { [[ :$comp_type: != *:[maA]:* && $value =~ ^.+/ ]] && COMP_PREFIX=$prefix${BASH_REMATCH[0]} - local ret cand + local ret cand "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe ble/complete/source:file/.construct-pathname-pattern "$value" ble/complete/util/eval-pathname-expansion "$ret"; (($?==148)) && return 148 ble/complete/source/test-limit ${#ret[@]} || return 1 + ble/complete/cand/yield.initialize file_rhs for cand in "${ret[@]}"; do [[ -e $cand || -h $cand ]] || continue [[ $FIGNORE ]] && ! ble/complete/.fignore/filter "$cand" && continue @@ -4416,7 +4771,8 @@ function ble/complete/source/compgen { # 既に完全一致している場合は、より前の起点から補完させるために省略 [[ $1 != '=' && ${#arr[@]} == 1 && $arr == "$COMPV" ]] && return 0 - local cand + local cand "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize "$action" for cand in "${arr[@]}"; do ((cand_iloop++%bleopt_complete_polling_cycle==0)) && ble/complete/check-cancel && return 148 ble/complete/cand/yield "$action" "$cand" "$data" @@ -4541,7 +4897,8 @@ function ble/complete/source:glob { ble/complete/source/eval-simple-word "$pattern*"; (($?==148)) && return 148 fi - local cand action=file + local cand action=file "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize "$action" for cand in "${ret[@]}"; do ((cand_iloop++%bleopt_complete_polling_cycle==0)) && ble/complete/check-cancel && return 148 ble/complete/cand/yield "$action" "$cand" @@ -4566,7 +4923,8 @@ function ble/complete/source:dynamic-history { local rex_wordbreaks='['$wordbreaks']' ble/util/assign-array ret 'HISTTIMEFORMAT= builtin history | ble/bin/grep -Eo "$rex_needle" | ble/bin/sed "s/^$rex_wordbreaks//" | ble/bin/sort -u' - local cand action=literal-word + local cand action=literal-word "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize "$action" for cand in "${ret[@]}"; do ((cand_iloop++%bleopt_complete_polling_cycle==0)) && ble/complete/check-cancel && return 148 ble/complete/cand/yield "$action" "$cand" @@ -6237,9 +6595,9 @@ function ble/complete/insert-braces { local beg=$COMP1 end=$COMP2 insert=$fixed$tail suffix= if [[ $comps_flags == *x* ]]; then - ble/complete/action/util/complete.addtail ',' + ble/complete/action/complete.addtail ',' else - ble/complete/action/util/complete.addtail ' ' + ble/complete/action/complete.addtail ' ' fi ble/complete/insert "$beg" "$end" "$insert" "$suffix" @@ -7393,7 +7751,7 @@ function ble/complete/sabbrev/expand { # construct cand_pack local cand_count cand_cand cand_word cand_pack ble/complete/candidates/clear - local cand COMP_PREFIX= + local COMP_PREFIX= # local settings local bleopt_sabbrev_menu_style=$bleopt_complete_menu_style @@ -7404,8 +7762,11 @@ function ble/complete/sabbrev/expand { # 或いは手動で ble/complete/cand/yield 等を呼び出してもらう。 local -a COMPREPLY=() builtin eval -- "$value" + + local cand action=word "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize "$action" for cand in "${COMPREPLY[@]}"; do - ble/complete/cand/yield word "$cand" "" + ble/complete/cand/yield "$action" "$cand" "" done if ((cand_count==0)); then @@ -7450,7 +7811,6 @@ function ble/complete/action:sabbrev/get-desc { } function ble/complete/source:sabbrev { local keys; ble/complete/sabbrev/get-keys - local key cand local filter_type=$comp_filter_type [[ $filter_type == none ]] && filter_type=head @@ -7460,6 +7820,8 @@ function ble/complete/source:sabbrev { local comp_filter_type local comp_filter_pattern ble/complete/candidates/filter#init "$filter_type" "$COMPS" + local cand action=sabbrev "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize "$action" for cand in "${keys[@]}"; do ble/complete/candidates/filter#test "$cand" || continue @@ -7470,7 +7832,7 @@ function ble/complete/source:sabbrev { local value=$ret # referenced in "ble/complete/action:sabbrev/initialize" local flag_source_filter=1 - ble/complete/cand/yield sabbrev "$cand" + ble/complete/cand/yield "$action" "$cand" done } @@ -7785,11 +8147,15 @@ function ble/cmdinfo/complete:cd/.impl { case $type in (pushd) if [[ $COMPV == - || $COMPV == -n ]]; then + local "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize "$action" ble/complete/cand/yield "$action" -n fi ;; (*) COMP_PREFIX=$COMPV local -a list=() + local "${_ble_complete_yield_varnames[@]/%/=}" # WA #D1570 safe + ble/complete/cand/yield.initialize "$action" [[ $COMPV == -* ]] && ble/complete/cand/yield "$action" "${COMPV}" [[ $COMPV != *L* ]] && ble/complete/cand/yield "$action" "${COMPV}L" [[ $COMPV != *P* ]] && ble/complete/cand/yield "$action" "${COMPV}P" diff --git a/note.txt b/note.txt index fc55dd01..c0c07577 100644 --- a/note.txt +++ b/note.txt @@ -1635,6 +1635,11 @@ bash_tips 2021-12-18 + * complete (source:command): quote していても alais が候補として生成されている + + * bleopt menu_align_{min,max} + これは別項目で議論する。 + * complete: FIGNORE と -o filenames どうやら元の bash では -o filenames が指定された時にのみ FIGNORE が使われる @@ -5687,6 +5692,128 @@ bash_tips 2021-12-18 + * menu-complete で 30s かかるという話 (2/2) yield quote-insert 高速化の試み [#D1714] + https://github.com/banoris/dotfiles/issues/11 + + #D1710 に於いて、設定を調整すれば以前より大分高速になるようにはしたが、それ + でも yield の速度が気になる。別項目で yield の高速化ができないか考察する事に + した。 + + * 先ず計測してみる事にする。 + + | * quote-insert をコメントアウトすると 0.63s かかっている。quote-insert + | があると 2.24s かかっている。うーん。3/4 ぐらいが quote にかかっている + | という事。一方で、quote 作業を gawk 等に任せたとして高速化の程度は限 + | られるし、また、gawk が出力した結果を読み出す為に追加の処理が必要にな + | る事に注意する。その様に考えると gawk で quote 処理を実施する事に積極 + | 的な意味は見いだせないかもしれない。 + | + | * 更に yield 関数の中身まで考えたらより高速化できる可能性もある。→然し + | 文字数を数えたり等の操作が入っているので solaris などの駄目な awk 実 + | 装で対応するのが難しい様な気がする。更に filter 等、より様々の事を実 + | 装する必要が生じる気がする。 + | + | 念の為 filter を抜いてみると 2.02s なので、filter のチェックに 0.22s + | かかっている。quote-insert にかかっているのが 1.61s だった。残りは + | 0.4s である。成程、両方ともくっつけたら大分短くできるのではないかとい + | う気がする。 + | + | * というか progcomp ばかり高速化しても仕方がない。全ての yield ループで + | 共通している項目は高速化できるのではないかという気がする。何れにしても + + yield loop の処理時間の内訳 + ------------ -------------------- + filter 0.22s (fignore, etc) + quote-insert 1.61s + 残り 0.40s + + * awk で実装してみて速度を比較する。 + + % うーん。試しに実装してみて比べるのはありかもしれないという程度。そうだ + % としても gawk を呼び出すのにかかる時間を計測して、ある程度以上の項目数 + % の時に呼び出す様に変更するのが望ましい。gawk を使ってすら重いという状況 + % については余り考えていない。というかそもそも '' 等で補完を開始しようと + % する事が間違っている様な気もする。 + + done: 先ずは各種の escape を awk で実装する必要がある。 + + →最終的に awk で色々 quote-insert する所まで実装できた。然し、awk の内部 + で test -f file 及び test -e file を判定する事ができないので、必要になる + 場合 (progcomp) の場合には予めテストを実行してからそれを awk に渡す事にす + る。 + + 外側でファイル判定する必要がない場合には 28ms であり、ファイル判定する必 + 要がある場合には 129ms である。更に読み出しの速度も考える必要がある。然し + そうだとしても速度的には十分速い。将来的には沢山の項目数の場合には awk に + 移行する事について考えて良いのではないかという気がする。 + + * 現在の quote-insert の実装も、外側でできるだけ準備をしてから、中では最低 + 限の判定だけで行ける様に修正する事ができる筈。 + + その様にした方が現在の awk による実装とも近くなり、両者の一貫性を保ちやす + くなる筈である。 + + →実装した。然し大して高速化していない。元々 2260ms 程度だったのが 2090ms + 程度に減少しただけである。つまり bottleneck はやはり其処ではないという事 + だろうか。 + + うーん。escape-for-bash-specialchars が遅いという事の気がしてきた。という + のも ' を入力した状態で補完するだけで 1600ms にまで減少する為。 + + * done: refact ble/complete/action/util/ ble/complete/action/? と思ったが色々 + の関数が既にある様だから、取り敢えずは現状を維持する事にする。と思ったが、 + やはり現在の action/inherit-from は action#inherit-from に改名して、代わ + りに action/util/ を action/ に書き換えた。 + + * done: 候補の数で awk を使うか使わないかを切り替える様にしたい + + ファイル数 従来 nawk mawk gawk + 100 24ms 10ms 10ms 12ms + 200 43ms 16ms 17ms 19ms + 500 106ms 35ms 39ms 41ms + 1k 212ms 67ms 76ms 78ms + 2k 424ms 128ms 147ms 150ms + 5k 1092ms 319ms 360ms 364ms + 10k 2094ms 626ms 720ms 723ms + + 適当に 500 項目以上の時に awk に切り替える事にした。 + 但し、cygwin/msys の場合には閾値は 2k にまで引き上げる事にする。 + + * 一般の場合に適用しようとすると改行の取り扱いに気をつけなければならない。 + + というか progcomp の場合だって \ を使って候補を繋ぐ事ができたという可能性? + と思ったが試しに compgen -f を実行してみたが、改行が含まれるファイル名を + そのまま出力してしまっていて、別々のファイル名の時との区別がつかない実装 + になっているので、progcomp に限っては余り気にしなくて良い様に思われる。 + + 一方で一般の場合には awk が \0 区切りに対応しているかどうかで実装方法が変 + わってくるのではないかという気がする。更に bash の version に依存して効率 + 的に読み取る事のできる形式も変わってくるだろう。 + + 取り敢えずは progcomp の場合だけ意識して実装して、改行に関しては注釈を加 + えるべきだろうと思われる。 + + * 改行が含まれている可能性があるかどうかの情報を渡してそれに応じて判定す + る様に変更した。今、quote-insert.batch は終了ステータス 1 を返した時に + 何もしない事に注意する。 + + * 更に quote-insert.batch が使える所ではできるだけ使える様にする→取り敢 + えず沢山の候補が生成されそうな場所は全て yield.batch に切り替える様にし + た。 + + * done: user-input で途中キャンセルできる様にする必要がある。 + →conditional-sync 経由で呼び出す様に変更した。動作確認した。 + + * done: xpg4 の sed でちゃんと "\\\\\\\\&" 置換が期待通りに動くかどうか確認 + する必要がある → "\\\\&" としなければならない様だ。その様にした。 + + x fixed: コマンド候補が全て赤く着色されている + + → conditional-sync にした時点で駄目になっている様だ。うーん。 + 出力結果を確認してみたがファイルは空である。何も出力していない。 + A & で起動したコマンドは、どうも外側でリダイレクトした fd から読み出せていない様だ。 + A の内部でリダイレクトする様に変更したら動く様になった。 + * ble/string#escape-for-bash-specialchars で HT を SP HT に置換しているのは何故? [#D1713] manu-complete 最適化中に気づいた事。 diff --git a/src/def.sh b/src/def.sh index f70b0648..44125ffd 100644 --- a/src/def.sh +++ b/src/def.sh @@ -73,6 +73,8 @@ blehook/declare syntax_load blehook/declare complete_load blehook/declare complete_insert +#------------------------------------------------------------------------------ + # for compatibility: function blehook/.compatibility-ble-0.3 { blehook keymap_load+='ble/util/invoke-hook _ble_keymap_default_load_hook' @@ -110,3 +112,8 @@ function blehook/.compatibility-ble-0.3/check { EOF fi } + +# Deprecated names +function ble/complete/action/inherit-from { + ble/complete/action#inherit-from "$@" +} diff --git a/src/util.sh b/src/util.sh index 5dc5e4cb..ad533cf5 100644 --- a/src/util.sh +++ b/src/util.sh @@ -469,10 +469,10 @@ function ble/debug/print-variables/.append-array { function ble/debug/print-variables { (($#)) || return 0 - local flags= tag= + local flags= tag= arg local -a _ble_local_vars=() while (($#)); do - local arg=$1; shift + arg=$1; shift case $arg in (-t) tag=$1; shift ;; (-*) ble/util/print "print-variables: unknown option '$arg'" >&2 @@ -2211,6 +2211,10 @@ function ble/util/writearray { REP_SL = "\\"; if (IS_XPG4) REP_SL = "\\\\"; + REP_DBL_SL = "\\\\"; + sub(/.*/, REP_DBL_SL, tmp); + if (tmp == "\\") REP_DBL_SL = "\\\\\\\\"; + s2i_initialize(); c2s_initialize(); es_initialize(); @@ -2218,6 +2222,10 @@ function ble/util/writearray { decl = ""; } + # Note: "str" must not contain "&" or "\\\\". When "&" is + # present, the escaping rule for "\\" changes in some awk. + # Now there is no problem because only DELIM (one character) is + # currently passed. function str2rep(str) { if (IS_XPG4) sub(/\\/, "\\\\\\\\", str); return str; @@ -2305,9 +2313,9 @@ function ble/util/writearray { head = head c2s(s2i(substr(s, 2, RLENGTH - 1), 16)); s = substr(s, RLENGTH + 1); } else if (match(s, /^c[ -~]/)) { -#% # \\c[ -~] (非ASCIIは未対応) + # \\c[ -~] (non-ascii characters are unsupported) c = es_s2c[substr(s, 2, 1)]; - head = head c2s(_ble_bash >= 40400 &&c == 63 ? 127 : c % 32); + head = head c2s(_ble_bash >= 40400 && c == 63 ? 127 : c % 32); s = substr(s, 3); } else { head = head "\\"; @@ -2403,7 +2411,7 @@ function ble/util/writearray { if (FLAG_NLFIX) { if (line ~ /\n/) { - gsub(/\\/, REP_SL REP_SL, line); + gsub(/\\/, REP_DBL_SL, line); gsub(/'\''/, REP_SL "'\''", line); gsub(/\a/, REP_SL "a", line); gsub(/\b/, REP_SL "b", line);