formatting fixes for parameter expansion - my favorite article.

This commit is contained in:
Sahal Ansari 2024-10-18 18:29:52 -05:00
parent 50aeb31ff8
commit 267eb1add1

View File

@ -52,47 +52,47 @@ For a more technical view what a parameter is and which types exist,
Looking for a specific syntax you saw, without knowing the name? Looking for a specific syntax you saw, without knowing the name?
- [Simple usage](#simple_usage) - [Simple usage](#simple-usage)
- `$PARAMETER` - `$PARAMETER`
- `${PARAMETER}` - `${PARAMETER}`
- [Indirection](#indirection) - [Indirection](#indirection)
- `${!PARAMETER}` - `${!PARAMETER}`
- [Case modification](#case_modification) - [Case modification](#case-modification)
- `${PARAMETER^}` - `${PARAMETER^}`
- `${PARAMETER^^}` - `${PARAMETER^^}`
- `${PARAMETER,}` - `${PARAMETER,}`
- `${PARAMETER,,}` - `${PARAMETER,,}`
- `${PARAMETER~}` - `${PARAMETER~}`
- `${PARAMETER~~}` - `${PARAMETER~~}`
- [Variable name expansion](#variable_name_expansion) - [Variable name expansion](#variable-name-expansion)
- `${!PREFIX*}` - `${!PREFIX*}`
- `${!PREFIX@}` - `${!PREFIX@}`
- [Substring removal](#substring_removal) (also for **filename - [Substring removal](#substring-removal) (also for **filename
manipulation**!) manipulation**!)
- `${PARAMETER#PATTERN}` - `${PARAMETER#PATTERN}`
- `${PARAMETER##PATTERN}` - `${PARAMETER##PATTERN}`
- `${PARAMETER%PATTERN}` - `${PARAMETER%PATTERN}`
- `${PARAMETER%%PATTERN}` - `${PARAMETER%%PATTERN}`
- [Search and replace](#search_and_replace) - [Search and replace](#search-and-replace)
- `${PARAMETER/PATTERN/STRING}` - `${PARAMETER/PATTERN/STRING}`
- `${PARAMETER//PATTERN/STRING}` - `${PARAMETER//PATTERN/STRING}`
- `${PARAMETER/PATTERN}` - `${PARAMETER/PATTERN}`
- `${PARAMETER//PATTERN}` - `${PARAMETER//PATTERN}`
- [String length](#string_length) - [String length](#string-length)
- `${#PARAMETER}` - `${#PARAMETER}`
- [Substring expansion](#substring_expansion) - [Substring expansion](#substring-expansion)
- `${PARAMETER:OFFSET}` - `${PARAMETER:OFFSET}`
- `${PARAMETER:OFFSET:LENGTH}` - `${PARAMETER:OFFSET:LENGTH}`
- [Use a default value](#use_a_default_value) - [Use a default value](#use-a-default-value)
- `${PARAMETER:-WORD}` - `${PARAMETER:-WORD}`
- `${PARAMETER-WORD}` - `${PARAMETER-WORD}`
- [Assign a default value](#assign_a_default_value) - [Assign a default value](#assign-a-default-value)
- `${PARAMETER:=WORD}` - `${PARAMETER:=WORD}`
- `${PARAMETER=WORD}` - `${PARAMETER=WORD}`
- [Use an alternate value](#use_an_alternate_value) - [Use an alternate value](#use-an-alternate-value)
- `${PARAMETER:+WORD}` - `${PARAMETER:+WORD}`
- `${PARAMETER+WORD}` - `${PARAMETER+WORD}`
- [Display error if null or unset](#display_error_if_null_or_unset) - [Display error if null or unset](#display-error-if-null-or-unset)
- `${PARAMETER:?WORD}` - `${PARAMETER:?WORD}`
- `${PARAMETER?WORD}` - `${PARAMETER?WORD}`
@ -226,14 +226,11 @@ The `^` operator modifies the first character to uppercase, the `,`
operator to lowercase. When using the double-form (`^^` and `,,`), all operator to lowercase. When using the double-form (`^^` and `,,`), all
characters are converted. characters are converted.
<wrap center round info 60%> !!! INFO
The (**currently undocumented**) operators `~` and `~~` reverse the case
The (**currently undocumented**) operators `~` and `~~` reverse the case of the given text (in `PARAMETER`).`~` reverses the case of first letter
of the given text (in `PARAMETER`).`~` reverses the case of first letter of words in the variable while `~~` reverses case for all. Thanks to
of words in the variable while `~~` reverses case for all. Thanks to `Bushmills` and `geirha` on the Freenode IRC channel for this finding.
`Bushmills` and `geirha` on the Freenode IRC channel for this finding.
</wrap>
<u>**Example: Rename all `*.txt` filenames to lowercase**</u> <u>**Example: Rename all `*.txt` filenames to lowercase**</u>
@ -270,9 +267,8 @@ Assume: `array=(This is some Text)`
- => `This Is Some Text` - => `This Is Some Text`
- `echo "${array[@]^^}"` - `echo "${array[@]^^}"`
- => `THIS IS SOME TEXT` - => `THIS IS SOME TEXT`
- `echo "${array[2]^^}"`
* ''echo "${array[2]^^}"'' - => `SOME`
* => ''SOME''
## Variable name expansion ## Variable name expansion
@ -351,16 +347,16 @@ filename**. Just look at the following list with examples:
- **Get name without extension** - **Get name without extension**
- `${FILENAME%.*}` - `${FILENAME%.*}`
- => `bash_hackers.txt` - => <code>bash_hackers<del>.txt</del></code>
- **Get extension** - **Get extension**
- `${FILENAME##*.}` - `${FILENAME##*.}`
- => `bash_hackers.txt` - => <code><del>bash_hackers.</del>txt</code>
- **Get directory name** - **Get directory name**
- `${PATHNAME%/*}` - `${PATHNAME%/*}`
- => `/home/bash/bash_hackers.txt` - => <code>/home/bash<del>/bash_hackers.txt</del></code>
- **Get filename** - **Get filename**
- `${PATHNAME##*/}` - `${PATHNAME##*/}`
- => `/home/bash/bash_hackers.txt` - => <code><del>/home/bash/</del>bash_hackers.txt</code>
These are the syntaxes for filenames with a single extension. Depending These are the syntaxes for filenames with a single extension. Depending
on your needs, you might need to adjust shortest/longest match. on your needs, you might need to adjust shortest/longest match.
@ -413,7 +409,7 @@ example string:
${MYSTRING//conservative/happy} ${MYSTRING//conservative/happy}
=> =>
`Be liberal in what you accept, and conservativehappy in what you send` <code>Be liberal in what you accept, and <del>conservative</del>happy in what you send</code>
Since there is only one "conservative" in that example, it really Since there is only one "conservative" in that example, it really
doesn't matter which of the two forms we use. doesn't matter which of the two forms we use.
@ -425,14 +421,14 @@ but let's substitute it with "by".
${MYSTRING/in/by} ${MYSTRING/in/by}
=> `Be liberal inby what you accept, and conservative in what you send` <code>Be liberal <del>in</del>by what you accept, and conservative by what you send</code>
<u>**Second form: Substitute all occurrences**</u> <u>**Second form: Substitute all occurrences**</u>
${MYSTRING//in/by} ${MYSTRING//in/by}
=> =>
`Be liberal inby what you accept, and conservative inby what you send` <code>Be liberal <del>in</del>by what you accept, and conservative <del>in</del>by what you send</code>
<u>**Anchoring**</u> Additionally you can "anchor" an <u>**Anchoring**</u> Additionally you can "anchor" an
expression: A `#` (hashmark) will indicate that your expression is expression: A `#` (hashmark) will indicate that your expression is
@ -536,7 +532,7 @@ that the offset 0 is the first character:
echo ${MYSTRING:35} echo ${MYSTRING:35}
=> =>
`<del>Be liberal in what you accept, and </del>conservative in what you send` <code><del>Be liberal in what you accept, and </del>conservative in what you send</code>
### Using Offset and Length ### Using Offset and Length
@ -545,7 +541,7 @@ In the second form we also give a length value:
echo ${MYSTRING:35:12} echo ${MYSTRING:35:12}
=> =>
`<del>Be liberal in what you accept, and </del>conservative<del> in what you send</del>` <code><del>Be liberal in what you accept, and </del>conservative<del> in what you send</del></code>
### Negative Offset Value ### Negative Offset Value
@ -570,7 +566,7 @@ then:
echo "${MYSTRING:11:-17}" echo "${MYSTRING:11:-17}"
=> =>
`<del>Be liberal </del>in what you accept, and conservative<del> in what you send</del>` <code><del>Be liberal </del>in what you accept, and conservative<del> in what you send</del></code>
This works since Bash 4.2-alpha, see also This works since Bash 4.2-alpha, see also
[bashchanges](../scripting/bashchanges.md). [bashchanges](../scripting/bashchanges.md).
@ -823,9 +819,11 @@ Removing the first 6 characters from a text string:
parameters plus the adjacent expansion are concatenated into a parameters plus the adjacent expansion are concatenated into a
single argument. As a workaround, each expansion needs to be quoted single argument. As a workaround, each expansion needs to be quoted
separately. Unfortunately, this bug took a very long time to separately. Unfortunately, this bug took a very long time to
notice.`~ $ set -- a b c; x=foo; printf '<%s> ' "$@$x" "$*""$x" "$@""$x" notice.
```
~ $ set -- a b c; x=foo; printf '<%s> ' "$@$x" "$*""$x" "$@""$x"
<a b cfoo> <a b cfoo> <a> <b> <cfoo> <a b cfoo> <a b cfoo> <a> <b> <cfoo>
` ```
- Almost all shells disagree about the treatment of an unquoted `$@`, - Almost all shells disagree about the treatment of an unquoted `$@`,
`${arr[@]}`, `$*`, and `${arr[*]}` when `${arr[@]}`, `$*`, and `${arr[*]}` when
@ -836,7 +834,9 @@ Removing the first 6 characters from a text string:
are few good reasons to leave `IFS` set to null for more than the are few good reasons to leave `IFS` set to null for more than the
duration of a command or two, and even fewer to expand `$@` and `$*` duration of a command or two, and even fewer to expand `$@` and `$*`
unquoted, this should be a rare issue. **Always quote unquoted, this should be a rare issue. **Always quote
them**!`touch x 'y z' them**!
```
touch x 'y z'
for sh in bb {{d,b}a,{m,}k,z}sh; do for sh in bb {{d,b}a,{m,}k,z}sh; do
echo "$sh" echo "$sh"
"$sh" -s a 'b c' d \* </dev/fd/0 "$sh" -s a 'b c' d \* </dev/fd/0
@ -848,7 +848,9 @@ Removing the first 6 characters from a text string:
printf "<%s> " $@ printf "<%s> " $@
echo echo
EOF EOF
``bb ```
```
bb
<ab cd*> <ab cd*>
<ab cd*> <ab cd*>
dash dash
@ -866,7 +868,8 @@ Removing the first 6 characters from a text string:
zsh zsh
<a> <b c> <d> <x> <y z> <a> <b c> <d> <x> <y z>
<a> <b c> <d> <x> <y z> <a> <b c> <d> <x> <y z>
`When `IFS` is set to a non-null value, or unset, all shells behave ```
When `IFS` is set to a non-null value, or unset, all shells behave
the same - first expanding into separate args, then applying the same - first expanding into separate args, then applying
pathname expansion and word-splitting to the results, except for pathname expansion and word-splitting to the results, except for
zsh, which doesn't do pathname expansion in its default mode. zsh, which doesn't do pathname expansion in its default mode.
@ -875,7 +878,9 @@ Removing the first 6 characters from a text string:
the behavior of inserting delimiter characters from IFS in `$*`, and the behavior of inserting delimiter characters from IFS in `$*`, and
the way adjacent arguments are concatenated, when IFS is modified in the way adjacent arguments are concatenated, when IFS is modified in
the middle of expansion through the middle of expansion through
side-effects.`for sh in bb {{d,b}a,po,{m,}k,z}sh; do side-effects.
```
for sh in bb {{d,b}a,po,{m,}k,z}sh; do
printf '%-4s: ' "$sh" printf '%-4s: ' "$sh"
"$sh" </dev/fd/0 "$sh" </dev/fd/0
done <<\EOF done <<\EOF
@ -885,14 +890,17 @@ Removing the first 6 characters from a text string:
printf '<%s> ' ${*}${IFS=}${*}${IFS:=-}"${*}" printf '<%s> ' ${*}${IFS=}${*}${IFS:=-}"${*}"
echo echo
EOF EOF
``bb : <a b cabc> <a-b-c> ```
```
bb : <a b cabc> <a-b-c>
dash: <a b cabc> <a-b-c> dash: <a b cabc> <a-b-c>
bash: <a> <b> <ca> <b> <c-a b c> bash: <a> <b> <ca> <b> <c-a b c>
posh: <a> <b> <ca b c> <a-b-c> posh: <a> <b> <ca b c> <a-b-c>
mksh: <a> <b> <ca b c> <a-b-c> mksh: <a> <b> <ca b c> <a-b-c>
ksh : <a> <b> <ca> <b> <c> <a b c> ksh : <a> <b> <ca> <b> <c> <a b c>
zsh : <a> <b> <ca> <b> <c> <a-b-c> zsh : <a> <b> <ca> <b> <c> <a-b-c>
`ksh93 and mksh can additionally achieve this side effect (and ```
ksh93 and mksh can additionally achieve this side effect (and
others) via the `${ cmds;}` expansion. I haven't yet tested every others) via the `${ cmds;}` expansion. I haven't yet tested every
possible side-effect that can affect expansion halfway through possible side-effect that can affect expansion halfway through
expansion that way. expansion that way.
@ -920,7 +928,8 @@ Removing the first 6 characters from a text string:
if the first part is out of range, the second won't be evaluated. if the first part is out of range, the second won't be evaluated.
ksh93 and mksh always evaluate the subscript parts even if the ksh93 and mksh always evaluate the subscript parts even if the
parameter is unset. parameter is unset.
` $ bash -c 'n="y[\$(printf yo >&2)1]" m="y[\$(printf jo >&2)1]"; x=(); echo "${x[@]:n,6:m}"' # No output ```
$ bash -c 'n="y[\$(printf yo >&2)1]" m="y[\$(printf jo >&2)1]"; x=(); echo "${x[@]:n,6:m}"' # No output
$ bash -c 'n="y[\$(printf yo >&2)1]" m="y[\$(printf jo >&2)1]"; x=([5]=hi); echo "${x[@]:n,6:m}"' $ bash -c 'n="y[\$(printf yo >&2)1]" m="y[\$(printf jo >&2)1]"; x=([5]=hi); echo "${x[@]:n,6:m}"'
yo yo
$ bash -c 'n="y[\$(printf yo >&2)1]" m="y[\$(printf jo >&2)1]"; x=([6]=hi); echo "${x[@]:n,6:m}"' $ bash -c 'n="y[\$(printf yo >&2)1]" m="y[\$(printf jo >&2)1]"; x=([6]=hi); echo "${x[@]:n,6:m}"'
@ -929,7 +938,7 @@ Removing the first 6 characters from a text string:
yojo yojo
$ bash -c 'n="y[\$(printf yo >&2)1]" m="y[\$(printf jo >&2)1]"; x=12345; echo "${x:n,6:m}"' $ bash -c 'n="y[\$(printf yo >&2)1]" m="y[\$(printf jo >&2)1]"; x=12345; echo "${x:n,6:m}"'
yo yo
` ```
### Quote Nesting ### Quote Nesting
@ -937,26 +946,30 @@ Removing the first 6 characters from a text string:
expansion that expands to multiple words, and nesting such expansion that expands to multiple words, and nesting such
expansions, not all combinations of nested quoting are possible. expansions, not all combinations of nested quoting are possible.
# Bash ```
$ typeset -a a=(meh bleh blerg) b # Bash
$ IFS=e $ typeset -a a=(meh bleh blerg) b
$ printf "<%s> " "${b[@]-"${a[@]}" "${a[@]}"}"; echo # The entire PE is quoted so Bash considers the inner quotes redundant. $ IFS=e
<meh> <bleh> <blerg meh> <bleh> <blerg> $ printf "<%s> " "${b[@]-"${a[@]}" "${a[@]}"}"; echo # The entire PE is quoted so Bash considers the inner quotes redundant.
$ printf "<%s> " "${b[@]-${a[@]} ${a[@]}}"; echo # The outer quotes cause the inner expansions to be considered quoted. <meh> <bleh> <blerg meh> <bleh> <blerg>
<meh> <bleh> <blerg meh> <bleh> <blerg> $ printf "<%s> " "${b[@]-${a[@]} ${a[@]}}"; echo # The outer quotes cause the inner expansions to be considered quoted.
$ b=(meep beep) <meh> <bleh> <blerg meh> <bleh> <blerg>
$ printf "<%s> " "${b[@]-"${a[@]}" "${a[@]}"}" "${b[@]-${a[@]} ${a[@]}}"; echo # Again no surprises. Outer quotes quote everything recursively. $ b=(meep beep)
<meep> <beep> <meep> <beep> $ printf "<%s> " "${b[@]-"${a[@]}" "${a[@]}"}" "${b[@]-${a[@]} ${a[@]}}"; echo # Again no surprises. Outer quotes quote everything recursively.
<meep> <beep> <meep> <beep>
```
Now lets see what can happen if we leave the outside unquoted. Now lets see what can happen if we leave the outside unquoted.
# Bash ```
$ typeset -a a=(meh bleh blerg) b # Bash
$ IFS=e $ typeset -a a=(meh bleh blerg) b
$ printf "<%s> " ${b[@]-"${a[@]}" "${a[@]}"}; echo # Inner quotes make inner expansions quoted. $ IFS=e
<meh> <bleh> <blerg meh> <bleh> <blerg> $ printf "<%s> " ${b[@]-"${a[@]}" "${a[@]}"}; echo # Inner quotes make inner expansions quoted.
$ printf "<%s> " ${b[@]-${a[@]} ${a[@]}}; echo' # No quotes at all wordsplits / globs, like you'd expect. <meh> <bleh> <blerg meh> <bleh> <blerg>
<m> <h> <bl> <h> <bl> <rg m> <h> <bl> <h> <bl> <rg> $ printf "<%s> " ${b[@]-${a[@]} ${a[@]}}; echo' # No quotes at all wordsplits / globs, like you'd expect.
<m> <h> <bl> <h> <bl> <rg m> <h> <bl> <h> <bl> <rg>
```
This all might be intuitive, and is the most common implementation, but This all might be intuitive, and is the most common implementation, but
this design sucks for a number of reasons. For one, it means Bash makes this design sucks for a number of reasons. For one, it means Bash makes