The issue where command substitution strips all trailing newlines comes up regularly (eg. on this list [1] and stackoverflow [2]). This behavior is historical and mandated by POSIX, and ensures that doing something like:
now=$(date) echo "$now" or: now=$(date) cat <<<$now is equivalent to just running date (since it would be very surprising if it wasn't). But, because $() strips (potentially) many trailing newlines, whereas echo or <<< add back only one newline, this breaks down when commands might output 0 or 2+ trailing newlines. There are at least 3 common workarounds: foo=$(some_command ... ; rc=$? ; echo . ; exit $rc) rc=$? foo=${foo%.} orig_REPLY=$(declare -p REPLY) read -rd '' < <(some_command ...) || [[ $REPLY ]] foo=$REPLY eval $orig_REPLY unset -v orig_REPLY mapfile < <(some_command ...) printf -v foo %s "${MAPFILE[@]}" In the past I have used these workarounds when necessary, however, IMHO they are all ugly, non-idiomatic, require relatively deep understanding of nuanced shell behavior, and are difficult to get exactly right, eg. the first needs hoop-jumping to retain $?, the second requires hoop-jumping to avoid clobbering $REPLY (and doing: read -rd '' foo < <(some_command ...) || [[ $foo ]] doesn't work as might be expected, I think because read with no names is a special case), and the third would require similar to avoid clobbering $MAPFILE. The ugliness can sometimes be hidden, eg. by encapsulating them into a function such as: function grab_output { if [ "$1" != output ]; then declare -n output="$1" fi shift output=$(eval "${@-cat}" ; rc=$? ; echo . ; exit $rc) local rc=$? output=${output%.} return $rc } but this still results in non-idiomatic code, because now you have to write: grab_output foo some_command ... or with shopt -s lastpipe: some_command ... | grab_output foo when what you actually mean (idiomatically) is: foo=$(some_command ...) I now have a use case which involves various combinations of command pipelines, with output sometimes stored in shell variables for inspection, before being finally output or fed into later pipelines. Bash is well suited for this, but in my use case: 1. The text input/output protocol between the various programs is line oriented, and sensitive to trailing newline stripping (ie. they should be preserved). (Briefly, each line is an independent input containing zero or more space-separated words, and an empty line with no words still means something specific and must be processed as usual. I can go into more detail if required.) 2. It's important that authors of these pipelines shouldn't need to be bash experts, and should be able to just use "normal" idiomatic bash code (ie. I don't want to have a library of helpers like grab_output, and have to tell authors that they have to remember to always use grab_output instead of doing foo=$(bar), etc etc). All of this means that what I really want is to tell bash not to strip trailing newlines from command substitution, and correspondingly to not add a trailing newline to here strings. (Here documents, being line-oriented with a line-based terminator, are unaffected, since their structure in the bash script is such that it always makes sense to add a trailing newline.) Attached is a patch which does this. The actual code changes are very simple - it adds two shopts (cmdsubst_trailing_nls which defaults to off, and herestr_trailing_nl which defaults to on), and two if statements which guard the code that strips/adds the trailing newlines. (The subst.c change looks worse because of the increased indentation, but diff -b shows that it's just wrapping the newline stripping code inside the newly added if statement.) I also added test suite coverage and documentation updates. Is this something that could be considered for adding to bash? I expect the maintenance burden from these changes would be low, but the main potential issues are (1) the choice of using shopts to modify behavior (vs alternative syntax ala [3]; each of these approaches has pros and cons), and (2) what the shopts ought to be named (for which I have no strong preference). As an illustrative example, without this change my scripts look like: shopt -s lastpipe some | complex | pipeline | grab_output my_result while :; do if is_any_good "$my_result"; then break else echo -n "$my_result" | some | further | processing | grab_output my_result fi done echo -n "$my_result" | final_processing whereas with the change, I think they are much more idiomatic, and basically identical to what I would write if trailing newlines were irrelevant (which is exactly what I'm aiming for): shopt -s cmdsubst_trailing_nls shopt -u herestr_trailing_nl my_result=$(some | complex | pipeline) while :; do if is_any_good "$my_result"; then break else my_result=$(some <<<$my_result | further | processing) fi done final_processing <<<$my_result Thanks, Kev [1] https://lists.gnu.org/archive/html/bug-bash/2007-05/msg00041.html https://lists.gnu.org/archive/html/bug-bash/2010-07/msg00090.html https://lists.gnu.org/archive/html/bug-bash/2017-06/msg00058.html https://lists.gnu.org/archive/html/bug-bash/2021-01/msg00259.html [2] https://unix.stackexchange.com/a/383411 https://unix.stackexchange.com/a/606739 [3] https://lists.gnu.org/archive/html/bug-bash/2021-01/msg00269.html
diff --git a/builtins/shopt.def b/builtins/shopt.def index cf6f6be..18811aa 100644 --- a/builtins/shopt.def +++ b/builtins/shopt.def @@ -96,6 +96,8 @@ extern int varassign_redir_autoclose; extern int singlequote_translations; extern int patsub_replacement; extern int bash_source_fullpath; +extern int cmdsubst_trailing_nls; +extern int herestr_trailing_nl; #if defined (EXTENDED_GLOB) extern int extended_glob; @@ -194,6 +196,7 @@ static struct { #if defined (HISTORY) { "cmdhist", &command_oriented_history, (shopt_set_func_t *)NULL }, #endif + { "cmdsubst_trailing_nls", &cmdsubst_trailing_nls, (shopt_set_func_t *)NULL }, { "compat31", &shopt_compat31, set_compatibility_level }, { "compat32", &shopt_compat32, set_compatibility_level }, { "compat40", &shopt_compat40, set_compatibility_level }, @@ -224,6 +227,7 @@ static struct { { "globskipdots", &glob_always_skip_dot_and_dotdot, (shopt_set_func_t *)NULL }, { "globstar", &glob_star, (shopt_set_func_t *)NULL }, { "gnu_errfmt", &gnu_error_format, (shopt_set_func_t *)NULL }, + { "herestr_trailing_nl", &herestr_trailing_nl, (shopt_set_func_t *)NULL }, #if defined (HISTORY) { "histappend", &force_append_history, (shopt_set_func_t *)NULL }, #endif @@ -378,6 +382,8 @@ reset_shopt_options (void) singlequote_translations = 0; patsub_replacement = PATSUB_REPLACE_DEFAULT; bash_source_fullpath = BASH_SOURCE_FULLPATH_DEFAULT; + cmdsubst_trailing_nls = 0; + herestr_trailing_nl = 1; #if defined (JOB_CONTROL) check_jobs_at_exit = 0; diff --git a/doc/bash.1 b/doc/bash.1 index 5af3492..0596c71 100644 --- a/doc/bash.1 +++ b/doc/bash.1 @@ -4119,7 +4119,11 @@ or (deprecated) .B Bash performs the expansion by executing \fIcommand\fP in a subshell environment and replacing the command substitution with the standard output of the -command, with any trailing newlines deleted. +command, with any trailing newlines deleted (unless +.B cmdsubst_trailing_nls +is set using the +.BR shopt (1) +builtin). Embedded newlines are not deleted, but they may be removed during word splitting. The command substitution \fB$(cat \fIfile\fP)\fR can be replaced by @@ -4143,7 +4147,10 @@ There is an alternate form of command substitution: .RE .PP which executes \fIcommand\fP in the current execution environment -and captures its output, again with trailing newlines removed. +and captures its output, again with trailing newlines removed (unless +.B cmdsubst_trailing_nls +is set using +.BR shopt (1)). .PP The character \fIc\fP following the open brace must be a space, tab, newline, or \fB|\fP, and the close brace must be in a position @@ -4168,7 +4175,9 @@ including the positional parameters, is shared with the caller. If the first character following the open brace is a \fB|\fP, the construct expands to the value of the \fBREPLY\fP shell variable after \fIcommand\fP executes, -without removing any trailing newlines, +without removing any trailing newlines (this behavior is not affected by the +.B cmdsubst_trailing_nls +shopt option), and the standard output of \fIcommand\fP remains the same as in the calling shell. \fBBash\fP creates \fBREPLY\fP as an initially-unset local variable when @@ -4957,7 +4966,12 @@ The \fIword\fP undergoes tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal. Pathname expansion and word splitting are not performed. -The result is supplied as a single string, with a newline appended, +The result is supplied as a single string, with a newline appended +(unless the +.B herestr_trailing_nl +shell option is unset using the +.BR shopt (1) +builtin), to the command on its standard input (or file descriptor \fIn\fP if \fIn\fP is specified). .SS "Duplicating File Descriptors" @@ -11921,6 +11935,14 @@ under .BR HISTORY . .PD 0 .TP 8 +.B cmdsubst_trailing_nls +If set, command substitution preserves trailing newlines from the command's +output before substitution occurs. If unset (the default), trailing newlines +are removed for the standard \fB$(\fP\fIcommand\fP\fB)\fP and legacy +\fB`\fP\fIcommand\fP\fB`\fP forms, as well as the alternate \fB${\fP +\fIcommand\fP\fB;}\fP form. The \fB${|\fP\fIcommand\fP\fB;}\fP form always +preserves trailing newlines in \fBREPLY\fP and is not affected by this option. +.TP 8 .B compat31 .TP 8 .B compat32 @@ -12121,6 +12143,11 @@ subdirectories match. If set, shell error messages are written in the standard GNU error message format. .TP 8 +.B herestr_trailing_nl +If set (the default), here strings (of the form \fB<<<\fP\fIword\fP) +have a trailing newline appended to \fIword\fP before being passed to +the command. If unset, the string is passed without modification. +.TP 8 .B histappend If set, the history list is appended to the file named by the value of the diff --git a/doc/bashref.texi b/doc/bashref.texi index 138a0f9..c687a01 100644 --- a/doc/bashref.texi +++ b/doc/bashref.texi @@ -2835,7 +2835,8 @@ or (deprecated) @noindent Bash performs command substitution by executing @var{command} in a subshell environment and replacing the command substitution with the standard -output of the command, with any trailing newlines deleted. +output of the command, with any trailing newlines deleted (unless the +@code{cmdsubst_trailing_nls} @code{shopt} option is set). Embedded newlines are not deleted, but they may be removed during word splitting. The command substitution @code{$(cat @var{file})} can be @@ -2857,7 +2858,8 @@ $@{@var{c} @var{command}; @} @noindent which executes @var{command} in the current execution environment -and captures its output, again with trailing newlines removed. +and captures its output, again with trailing newlines removed (unless +the @code{cmdsubst_trailing_nls} @code{shopt} option is set). The character @var{c} following the open brace must be a space, tab, newline, or @samp{|}, and the close brace must be in a position @@ -2882,7 +2884,8 @@ including the positional parameters, is shared with the caller. If the first character following the open brace is a @samp{|}, the construct expands to the value of the @code{REPLY} shell variable after @var{command} executes, -without removing any trailing newlines, +without removing any trailing newlines (this behavior is not affected by +the @code{cmdsubst_trailing_nls} @code{shopt} option), and the standard output of @var{command} remains the same as in the calling shell. Bash creates @code{REPLY} as an initially-unset local variable when @@ -3542,7 +3545,8 @@ The @var{word} undergoes tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal. Filename expansion and word splitting are not performed. -The result is supplied as a single string, with a newline appended, +The result is supplied as a single string, with a newline appended +(unless the @code{herestr_trailing_nl} shell option is unset), to the command on its standard input (or file descriptor @var{n} if @var{n} is specified). @@ -6326,6 +6330,15 @@ This allows easy re-editing of multi-line commands. This option is enabled by default, but only has an effect if command history is enabled (@pxref{Bash History Facilities}). +@item cmdsubst_trailing_nls +If set, command substitution preserves trailing newlines from the +command's output before substitution occurs. If unset (the default), +trailing newlines are removed for the standard @code{$(@var{command})} +and legacy @code{`@var{command}`} forms, as well as the alternate +@code{$@{ @var{command}; @}} form. The @code{$@{| @var{command}; @}} +form always preserves trailing newlines in @code{REPLY} and is not +affected by this option. + @item compat31 @itemx compat32 @itemx compat40 @@ -6465,6 +6478,11 @@ subdirectories match. If set, shell error messages are written in the standard @sc{gnu} error message format. +@item herestr_trailing_nl +If set (the default), here strings (of the form @code{<<< @var{word}}) +have a trailing newline appended to @var{word} before being passed to +the command. If unset, the string is passed without modification. + @item histappend If set, the history list is appended to the file named by the value of the @env{HISTFILE} diff --git a/redir.c b/redir.c index 343536b..0973c84 100644 --- a/redir.c +++ b/redir.c @@ -88,6 +88,9 @@ extern int errno; int expanding_redir; int varassign_redir_autoclose = 0; +/* If non-zero, here-strings append a trailing newline. */ +int herestr_trailing_nl = 1; + extern REDIRECT *redirection_undo_list; extern REDIRECT *exec_redirection_undo_list; @@ -383,8 +386,8 @@ heredoc_expand (WORD_DESC *redirectee, enum r_instruction ri, size_t *lenp) executing_builtin = old; dlen = STRLEN (document); - /* XXX - Add trailing newline to here-string */ - if (ri == r_reading_string) + /* Add trailing newline to here-string unless disabled by shopt */ + if (ri == r_reading_string && herestr_trailing_nl) { document = xrealloc (document, dlen + 2); document[dlen++] = '\n'; diff --git a/subst.c b/subst.c index e9d3e75..f1126e6 100644 --- a/subst.c +++ b/subst.c @@ -197,6 +197,9 @@ int fail_glob_expansion; pattern substitution word expansion. */ int patsub_replacement = PATSUB_REPLACE_DEFAULT; +/* If non-zero, command substitution preserves trailing newlines. */ +int cmdsubst_trailing_nls = 0; + /* Are we executing a ${ command; } nofork comsub? */ int executing_funsub = 0; @@ -6770,37 +6773,41 @@ read_comsub (int fd, int quoted, int flags, int *rflag) return (char *)NULL; } - /* Strip trailing newlines from the output of the command. */ - if (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) + /* Strip trailing newlines from the output of the command unless + preservation is enabled via shopt cmdsubst_trailing_nls. */ + if (cmdsubst_trailing_nls == 0) { - while (istring_index > 0) + if (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES)) { - if (istring[istring_index - 1] == '\n') + while (istring_index > 0) { - --istring_index; - - /* If the newline was quoted, remove the quoting char. */ - if (istring[istring_index - 1] == CTLESC) - --istring_index; - -#ifdef __MSYS__ - if (istring_index > 0 && istring[istring_index - 1] == '\r') + if (istring[istring_index - 1] == '\n') { --istring_index; - /* If the carriage return was quoted, remove the quoting char. */ + /* If the newline was quoted, remove the quoting char. */ if (istring[istring_index - 1] == CTLESC) --istring_index; - } + +#ifdef __MSYS__ + if (istring_index > 0 && istring[istring_index - 1] == '\r') + { + --istring_index; + + /* If the carriage return was quoted, remove the quoting char. */ + if (istring[istring_index - 1] == CTLESC) + --istring_index; + } #endif + } + else + break; } - else - break; + istring[istring_index] = '\0'; } - istring[istring_index] = '\0'; + else + strip_trailing (istring, istring_index - 1, 1); } - else - strip_trailing (istring, istring_index - 1, 1); if (rflag) *rflag = tflag; diff --git a/tests/complete.right b/tests/complete.right index 1b7893b..a70e2e9 100644 --- a/tests/complete.right +++ b/tests/complete.right @@ -182,6 +182,7 @@ checkhash checkjobs checkwinsize cmdhist +cmdsubst_trailing_nls compat31 compat32 compat40 @@ -204,6 +205,7 @@ globasciiranges globskipdots globstar gnu_errfmt +herestr_trailing_nl histappend histreedit histverify diff --git a/tests/comsub-newlines.right b/tests/comsub-newlines.right new file mode 100644 index 0000000..60e5910 --- /dev/null +++ b/tests/comsub-newlines.right @@ -0,0 +1,9 @@ +[A + +] +[B + +] +[C + +] diff --git a/tests/comsub-newlines.tests b/tests/comsub-newlines.tests new file mode 100644 index 0000000..485f79e --- /dev/null +++ b/tests/comsub-newlines.tests @@ -0,0 +1,22 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Verify that enabling cmdsubst_trailing_nls preserves trailing newlines +# in command substitutions for legacy backticks, $(), and ${ cmd; } forms. + +shopt -s cmdsubst_trailing_nls + +echo "[`printf 'A\n\n'`]" +echo "[$(printf 'B\n\n')]" +echo "[${ printf 'C\n\n'; }]" + diff --git a/tests/herestr-trailing.right b/tests/herestr-trailing.right new file mode 100644 index 0000000..9e86d31 --- /dev/null +++ b/tests/herestr-trailing.right @@ -0,0 +1,21 @@ +Testing herestr_trailing_nl behavior +Default behavior: +declare -a MAPFILE=([0]=$'test\n') +Disabled behavior: +declare -a MAPFILE=([0]="test") +Empty string default: +declare -a MAPFILE=([0]=$'\n') +Empty string disabled: +declare -a MAPFILE=() +Multiple words default: +declare -a MAPFILE=([0]=$'hello world\n') +Multiple words disabled: +declare -a MAPFILE=([0]="hello world") +Multiline default: +declare -a MAPFILE=([0]=$'foo\n' [1]=$'bar\n') +Multiline disabled: +declare -a MAPFILE=([0]=$'foo\n' [1]="bar") +Multiline with trailing newline default: +declare -a MAPFILE=([0]=$'foo\n' [1]=$'bar\n' [2]=$'\n') +Multiline with trailing newline disabled: +declare -a MAPFILE=([0]=$'foo\n' [1]=$'bar\n') diff --git a/tests/herestr-trailing.tests b/tests/herestr-trailing.tests new file mode 100644 index 0000000..dbcaee0 --- /dev/null +++ b/tests/herestr-trailing.tests @@ -0,0 +1,75 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Test herestr_trailing_nl shopt option behavior + +echo "Testing herestr_trailing_nl behavior" + +# Test 1: Default behavior (should add trailing newline) +echo "Default behavior:" +mapfile <<<"test" +declare -p MAPFILE + +# Test 2: Disabled behavior (should not add trailing newline) +echo "Disabled behavior:" +shopt -u herestr_trailing_nl +mapfile <<<"test" +declare -p MAPFILE +shopt -s herestr_trailing_nl + +# Test 3: Empty string - default +echo "Empty string default:" +mapfile <<<"" +declare -p MAPFILE + +# Test 4: Empty string - disabled +echo "Empty string disabled:" +shopt -u herestr_trailing_nl +mapfile <<<"" +declare -p MAPFILE +shopt -s herestr_trailing_nl + +# Test 5: Multiple words - default +echo "Multiple words default:" +mapfile <<<"hello world" +declare -p MAPFILE + +# Test 6: Multiple words - disabled +echo "Multiple words disabled:" +shopt -u herestr_trailing_nl +mapfile <<<"hello world" +declare -p MAPFILE + +# Test 7: Multiline string - default +echo "Multiline default:" +shopt -s herestr_trailing_nl +mapfile <<<$'foo\nbar' +declare -p MAPFILE + +# Test 8: Multiline string - disabled +echo "Multiline disabled:" +shopt -u herestr_trailing_nl +mapfile <<<$'foo\nbar' +declare -p MAPFILE + +# Test 9: Multiline with trailing newline - default +echo "Multiline with trailing newline default:" +shopt -s herestr_trailing_nl +mapfile <<<$'foo\nbar\n' +declare -p MAPFILE + +# Test 10: Multiline with trailing newline - disabled +echo "Multiline with trailing newline disabled:" +shopt -u herestr_trailing_nl +mapfile <<<$'foo\nbar\n' +declare -p MAPFILE diff --git a/tests/invocation.right b/tests/invocation.right index 304fc91..aa85f22 100644 --- a/tests/invocation.right +++ b/tests/invocation.right @@ -72,9 +72,9 @@ Shell options: this-bash this-bash $- for -c includes c bash: line 0: badopt: invalid shell option name -checkwinsize:cmdhist:complete_fullquote:extquote:force_fignore:globasciiranges:globskipdots:hostcomplete:interactive_comments:patsub_replacement:progcomp:promptvars:sourcepath -checkhash:checkwinsize:cmdhist:complete_fullquote:extquote:force_fignore:globasciiranges:globskipdots:hostcomplete:interactive_comments:patsub_replacement:progcomp:promptvars:sourcepath -cmdhist:complete_fullquote:extquote:force_fignore:globasciiranges:globskipdots:hostcomplete:interactive_comments:patsub_replacement:progcomp:promptvars:sourcepath +checkwinsize:cmdhist:complete_fullquote:extquote:force_fignore:globasciiranges:globskipdots:herestr_trailing_nl:hostcomplete:interactive_comments:patsub_replacement:progcomp:promptvars:sourcepath +checkhash:checkwinsize:cmdhist:complete_fullquote:extquote:force_fignore:globasciiranges:globskipdots:herestr_trailing_nl:hostcomplete:interactive_comments:patsub_replacement:progcomp:promptvars:sourcepath +cmdhist:complete_fullquote:extquote:force_fignore:globasciiranges:globskipdots:herestr_trailing_nl:hostcomplete:interactive_comments:patsub_replacement:progcomp:promptvars:sourcepath ./invocation1.sub: line 40: BASHOPTS: readonly variable braceexpand:hashall:interactive-comments braceexpand:hashall:interactive-comments diff --git a/tests/run-comsub-newlines b/tests/run-comsub-newlines new file mode 100644 index 0000000..17a07b0 --- /dev/null +++ b/tests/run-comsub-newlines @@ -0,0 +1,2 @@ +${THIS_SH} ./comsub-newlines.tests > ${BASH_TSTOUT} 2>&1 +diff ${BASH_TSTOUT} comsub-newlines.right && rm -f ${BASH_TSTOUT} diff --git a/tests/run-herestr-trailing b/tests/run-herestr-trailing new file mode 100755 index 0000000..996989f --- /dev/null +++ b/tests/run-herestr-trailing @@ -0,0 +1,2 @@ +${THIS_SH} ./herestr-trailing.tests > ${BASH_TSTOUT} 2>&1 +diff ${BASH_TSTOUT} herestr-trailing.right && rm -f ${BASH_TSTOUT} diff --git a/tests/shopt.right b/tests/shopt.right index 2d47b57..d70b195 100644 --- a/tests/shopt.right +++ b/tests/shopt.right @@ -11,6 +11,7 @@ shopt -u checkhash shopt -u checkjobs shopt -u checkwinsize shopt -s cmdhist +shopt -u cmdsubst_trailing_nls shopt -u compat31 shopt -u compat32 shopt -u compat40 @@ -33,6 +34,7 @@ shopt -s globasciiranges shopt -s globskipdots shopt -u globstar shopt -u gnu_errfmt +shopt -s herestr_trailing_nl shopt -u histappend shopt -u histreedit shopt -u histverify @@ -73,6 +75,7 @@ shopt -s extquote shopt -s force_fignore shopt -s globasciiranges shopt -s globskipdots +shopt -s herestr_trailing_nl shopt -s hostcomplete shopt -s interactive_comments shopt -s patsub_replacement @@ -88,6 +91,7 @@ shopt -u cdable_vars shopt -u checkhash shopt -u checkjobs shopt -u checkwinsize +shopt -u cmdsubst_trailing_nls shopt -u compat31 shopt -u compat32 shopt -u compat40 @@ -134,6 +138,7 @@ cdable_vars off checkhash off checkjobs off checkwinsize off +cmdsubst_trailing_nls off compat31 off compat32 off compat40 off