> --- a/CHANGES
> +++ b/CHANGES
> @@ -1,3 +1,326 @@
> +This document details the changes between this version, bash-5.2-alpha, and
> +the previous version, bash-5.1-release.
>
> [...]
>
> +x. New shell option: patsub_replacement. When enabled, a `&' in the 
> replacement
> +   string of the pattern substitution expansion is replaced by the portion of
> +   the string that matched the pattern. Backslash will escape the `&' and
> +   insert a literal `&'.
>
> [...]

I haven't received replies to my previous reply, but let me write a
related discussion again. The current behavior is as follows:

$ bash-dev --norc
$ shopt | grep patsub
patsub_replacement      on
$ var=ABCDE value='(K&R)'
$ echo "${var//C/$value}"
AB(KCR)DE
$ echo "${var//C/"$value"}"     # (My suggestion of "quoting &")
AB(KCR)DE     # (the current patsub_replacement isn't designed to work for this)
$ echo "${var//C/(K\\&R)}"
AB(K&R)DE
$ echo "${var//C/\\\\\\\\}"
AB\\\\DE
$ echo "${var//C/\\\\\\\\&}"
AB\\CDE

Today I tried to modify my script so that it works with `shopt -s
patsub_replacement', and now I become more skeptical about making
patsub_replacement default and also about its current design.

I needed to modify more places than I initially expected, and also it
is not simple to simply perform the replacement that contains
arbitrary strings. Then, I checked other existing Bash programs and
found that most of the large Bash programs/frameworks are affected,
which include "bashdb", "bash-completion", "bash-it",
"bash-oo-framework", "neofetch", and "bashtop" (plus mine, "ble.sh").
The programs that seemed to be fine among the ones I have checked were
only "oh-my-bash", "romkatv/gitstatus" and "git-prompt.bash". So I
feel it is better to make patsub_replacement off by default. I attach
"affected.txt", the list of lines from these programs that can be
broken by "patsub_replacement" depending on the user inputs,
configurations and the system status:

Also, it is not simple to correctly write the codes so that it works
for both sides of patsub_replacement on/off. If it was a standalone
Bash program, we can just set `shopt -u patsub_replacement' at the
beginning of the script file, but debuggers (bashdb), shell
configurations (bash-completion, bash-it, ble.sh, etc.) and shell
libraries (shell libraries, ...) cannot assume one specific side of
the settings of `patsub_replacement' because that option is under the
control of the main program or the user of the interactive shell. I
have thought about how we can correctly write, e.g., «
result=${result// /$string} » as simply as possible (see the attached
"workaround.sh"), but I still feel they are all too non-trivial to
replace the simple « result=${result// /$string} ». I don't think
everyone can write it correctly.

Sorry, but I'd like to still push treating the quoted replacement (as
$string in « result=${result// /$string} ») literally just like the
glob operators in ${var//"$pat"} or ${var#"$pat"} (as recently
explained in e.g.
https://lists.gnu.org/archive/html/help-bash/2022-01/msg00022.html).
In Chet's previous reply, it is explained that

> > > * It is consistent with the treatment of the glob special characters
> > >   and anchors # and % in $pat of ${var/$pat}.
> >
> > Yeah, doing that was probably a mistake, but we have to live with it now.
> > Those are really part of the pattern operator itself, not properties of
> > the pattern. But nevertheless.

but I don't feel it is a mistake but rather natural than introducing
an extra level of unescaping/unquoting. It might be just because I got
used to it, but

> However, if I understand it correctly, similar treatment is
> already standardized in POSIX for ``quoting characters within the
> braces'' (as ${var#"$xxx"} and ${var%"$xxx"}):
>
> > quoted from POSIX XCU 2.6.2
> > https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html#tag_18_06_02
> > [...]

which means most shell implementations agreed with its behavior for
glob operators in ${var#pat} and ${var%pat}.
./bashdb-5.0-1.1.2/lib/fns.sh:33:    result=${result// /$string}

./bash-completion/bash_completion:741:    ((${#COMPREPLY[@]} == 1)) && 
COMPREPLY=(${COMPREPLY/#/$prefix})
./bash-completion/bash_completion:920:        
option=${option/"${BASH_REMATCH[1]}"/"${BASH_REMATCH[2]}"}
./bash-completion/bash_completion:955:                
line=${line/"${BASH_REMATCH[0]}"/"${BASH_REMATCH[1]}"}
./bash-completion/bash_completion:1020:    
COMPREPLY+=("${sigs[@]/#${1-}SIG/${1-}}")
./bash-completion/completions/_mount.linux:27:            $split && 
COMPREPLY=(${COMPREPLY[@]/#/$prev,})
./bash-completion/completions/_mount.linux:209:            $split && 
COMPREPLY=(${COMPREPLY[@]/#/"$prev,"})
./bash-completion/completions/_umount.linux:119:            $split && 
COMPREPLY=(${COMPREPLY[@]/#/$prev,})
./bash-completion/completions/chromium-browser:21:                    
COMPREPLY=("${COMPREPLY[@]/#/$prefix}")
./bash-completion/completions/cppcheck:29:            $split && 
COMPREPLY=(${COMPREPLY[@]/#/"$prev,"})
./bash-completion/completions/cvs:9:        
entries=("${entries[@]/#/${prefix:-}}")
./bash-completion/completions/info:52:        infopath="${infopath//://$cur* }"
./bash-completion/completions/kcov:38:                    
COMPREPLY=(${COMPREPLY/#/$prev,})
./bash-completion/completions/man:80:        
manpath="${manpath//://*man$sect/$cur* } ${manpath//://*cat$sect/$cur* }"
./bash-completion/completions/man:82:        manpath="${manpath//://*man$sect/ 
} ${manpath//://*cat$sect/ }"
./bash-completion/completions/mutt:121:        [[ -n $spoolfile ]] && eval 
cur="${cur/^!/$spoolfile}"
./bash-completion/completions/povray:19:            
COMPREPLY=(${COMPREPLY[@]/#/$pfx})
./bash-completion/completions/povray:39:            
COMPREPLY=(${COMPREPLY[@]/%.pov/.$oext})
./bash-completion/completions/povray:43:            
COMPREPLY=(${COMPREPLY[@]/#/$pfx})
./bash-completion/completions/povray:55:            
COMPREPLY=("${COMPREPLY[@]/#/${pfx}[}")
./bash-completion/completions/pylint:87:            ((${#COMPREPLY[@]} == 1)) 
&& COMPREPLY=(${COMPREPLY/#/$prefix})
./bash-completion/completions/smartctl:91:    [[ -n $prefix ]] && 
COMPREPLY=("${COMPREPLY[@]/#/$prefix}")

./bash-it/completion/available/sdkman.completion.bash:15:                       
        CANDIDATES="${SDKMAN_CANDIDATES_CSV//,/${IFS:0:1}}"
./bash-it/plugins/available/alias-completion.plugin.bash:87:                    
    COMP_LINE=\${COMP_LINE/$alias_name/$alias_cmd $alias_args}

./bash-oo-framework/lib/util/exception.sh:157:  local 
underlinedObjectInLine="${errLine/$stringToMarkWithoutSlash/$underlinedObject}"

./neofetch/neofetch:3012:        song="${song_format/\%artist\%/$artist}"
./neofetch/neofetch:3013:        song="${song/\%album\%/$album}"
./neofetch/neofetch:3014:        song="${song/\%title\%/$title}"
./neofetch/neofetch:4087:    ascii_data="${ascii_data//\$\{c1\}/$c1}"
./neofetch/neofetch:4088:    ascii_data="${ascii_data//\$\{c2\}/$c2}"
./neofetch/neofetch:4089:    ascii_data="${ascii_data//\$\{c3\}/$c3}"
./neofetch/neofetch:4090:    ascii_data="${ascii_data//\$\{c4\}/$c4}"
./neofetch/neofetch:4091:    ascii_data="${ascii_data//\$\{c5\}/$c5}"
./neofetch/neofetch:4092:    ascii_data="${ascii_data//\$\{c6\}/$c6}"
./neofetch/neofetch:4611:    
string="${string/:/${reset}${colon_color}${separator:=:}${info_color}}"
./neofetch/neofetch:4628:                        "${underline// 
/$underline_char}${reset} "
./neofetch/neofetch:4808:    bar+="${bar_color_elapsed}${prog// 
/${bar_char_elapsed}}"
./neofetch/neofetch:4809:    bar+="${bar_color_total}${total// 
/${bar_char_total}}"

./bashtop/bashtop:953:                                  
line_array=(${input_line/${key}/${key// /}})
./bashtop/bashtop:994:                  math="${math//x/$found}"
./bashtop/bashtop:1933:                 text="${r_tmp// /$text}"
./bashtop/bashtop:2356: proc_array[0]="${proc_array[0]/ 
${selected}/${symbol}${selected}}"
./bashtop/bashtop:2624: proc_array[0]="${proc_array[0]/ 
${selected}/${symbol}${selected}}"

./sentaku/sentaku:829:          
_s_show="${_s_show//$w/\\e[31${negative}m$w\\e[${negative}m}"

./sbp/segments/git.bash:55:        outgoing_filled="${upstream_stripped/ahead 
/${outgoing_icon}}"
./sbp/segments/git.bash:56:        upstream_status="${outgoing_filled/behind 
/${incoming_icon}}"

./ble.sh/keymap/vi.sh:6974:    ins=${ins//[!$'\n']/"$s"}
./ble.sh/lib/core-syntax.sh:976:      a=${a//@h/$histc1}
./ble.sh/lib/core-syntax.sh:977:      a=${a//@q/$histc2}
./ble.sh/lib/core-syntax.sh:982:    a=${a//@h/$histc1}
./ble.sh/lib/core-syntax.sh:983:    a=${a//@q/$histc2}
./ble.sh/lib/core-syntax.sh:2255:    rex_event=${rex_event//@A/$A}
./ble.sh/lib/core-syntax.sh:2266:    rex_quicksub=${rex_quicksub//@A/[$histc2]}
./ble.sh/lib/core-syntax.sh:2267:    rex_quicksub=${rex_quicksub//@C/$histc2}
./ble.sh/lib/core-syntax.sh:4453:    a=\\   ; b="\\$a"; ret="${ret//"$a"/$b}"
./ble.sh/src/edit.sh:604:    a='\' b='\\' text=${text//"$a"/$b}
./ble.sh/src/util.sh:118:          ble/array#push specs "${var[@]/%/=$value}" # 
#D1570 WA checked
./ble.sh/src/util.sh:777:    ARR=("${ARR[@]::$2}" "${sARR[@]/#/$4}" 
"${ARR[@]:$3}")' # WA #D1570 checked
./ble.sh/src/util.sh:806:  ret="${ret// /$1}"
./ble.sh/src/util.sh:1083:      a=${chars1:i:1} b=\\${chars2:i:1} 
ret=${ret//"$a"/$b}
./ble.sh/src/util.sh:5305:  ble/util/put 
"${_ble_term_visible_bell_show//'%message%'/$sgr$message}" >&2
./ble.sh/src/util.sh:5622:  local ret=${_ble_term_Ss//@1/$state}
#!/bin/bash

function fn_broken {
  result=${result// /$string}
}

# solution 1 (need to write the same parameter expansions twice)
function fn_v1 {
  if shopt -q patsub_replacement 2>/dev/null; then
    shopt -u patsub_replacement
    result=${result// /$string}
    shopt -s patsub_replacement
  else
    result=${result// /$string}
  fi
}

# solution 2
function fn_v2 {
  if shopt -q patsub_replacement 2>/dev/null; then
    shopt -u patsub_replacement
    "$FUNCNAME" "$@"
    local status=$?
    shopt -s patsub_replacement
    return "$status"
  else
    result=${result// /$string}
  fi
}

# solution 3 (there is a fork cost)
function fn_v3 {
  local reset=$(shopt -p patsub_replacement 2>/dev/null)
  shopt -u patsub_replacement 2>/dev/null
  result=${result// /$string}
  eval -- "$reset"
}

# solution 4
function fn_v4 {
  local shopt=$BASHOPTS
  shopt -u patsub_replacement 2>/dev/null
  result=${result// /$string}
  [[ :$shopt: != *:patsub_replacement:* ]] || shopt -s patsub_replacement
}

# solution 5
function fn_v5 {
  local reset=
  if shopt -q patsub_replacement &>/dev/null; then
    shopt -u patsub_replacement
    reset='shopt -s patsub_replacement'
  fi
  result=${result// /$string}
  eval "$reset"
}

Reply via email to