From f650f13a0e3fc9761a46561cea542207cc480d4b Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 22 Aug 2021 21:12:02 -0700 Subject: [PATCH] Created SC2308 (markdown) --- SC2308.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 SC2308.md diff --git a/SC2308.md b/SC2308.md new file mode 100644 index 0000000..c9be31e --- /dev/null +++ b/SC2308.md @@ -0,0 +1,100 @@ +## `expr length` has unspecified results. Prefer `${#var}`. + +or `'expr match' has unspecified results. Prefer 'expr str : regex'.` +or `'expr substr' has unspecified results. Prefer 'cut' or ${var#???}.` +or `'expr index' has unspecified results. Prefer x=${var%%[chars]*}; $((${#x}+1)).` + +### Problematic code: + +```sh +# Find length of string +length=$(expr length "$var") + +# Match string against regex +expr match "$input" "[0-9]*" + +# Find character index in string +pos=$(expr index "$input" ":") + +# Get substring by index +col2=$(expr substr "foo bar baz" 8 5) +``` + +### Correct code: + +```sh +# Find length of string +length=${#var} + +# Match string against regex +expr "$input" : "[0-9]*" + +# Find character index in string +pos=${input%%:*} pos=$((${#pos}+1)) + +# Get substring by index (bash) +str="foo bar baz" +col2="${str:7:5}" + +# Get substring by index (POSIX) +col2="$(printf 'foo bar baz\n' | cut -c 8-12)" +``` +### Rationale: + +You are using a `expr` with `length`, `match`, `index`, or `substr`. These forms did not make it into POSIX, and fail on platforms like MacOS and FreeBSD. Consider replacing them with portable equivalents: + +#### `length` +can be trivially replaced with `${#var}` + +#### `match` +can be trivially replaced with the POSIX form `expr str : regex` + +#### `index` +if you only need a numerical index as part of trying to extract a piece of the string, consider replacing it with parameter expansion: + +``` +str="mykey=myvalue" +key="${str%%=*}" # Remove everything after first =, no index required +value="${str#*=}" # Remove everything before first =, no index required +``` + +otherwise, you can find the index of the first `=` using parameter expansion and string length: + +``` +str="mykey=myvalue" +x=${str%%=*} # Assign x="mystr" +index=$((x+1)) # Add 1 to length of x +``` + +#### `substr` + +Extract a substring via character index is generally fragile. For example, in this example, any minor changes to the format, including just the version increasing from 8.9 to 8.10, will cause the following snippet to fail: + +``` +str="VIM - Vi IMproved 8.2 (2019 Dec 12, compiled Feb 15 2021 12:29:39)" +version=$(expr substr "$str" 19 3) +``` + +Instead, consider a different approach: + +``` +x="${str%% (*}" # Delete ` (` and everything after, giving "VIM - Vi IMproved 8.2" +version="${x##* }" # Delete everything before last space, giving "8.2" + +# Get the fifth word separated by spaces +IFS=" " read -r _ _ _ _ version _ << EOF +$str +EOF +``` + +If you still want to use character index, this is trivially done in Bash/Ksh with `${var:offset:length}` (0-based). + +In POSIX, you can generally use `cut`, though be careful if the value can contain multiple lines. + +### Exceptions: + +If you know your script will only run on platforms where these forms are supported, like GNU or BusyBox, you can [[ignore]] this warning. + +### Related resources: + +* Help by adding links to BashFAQ, StackOverflow, man pages, POSIX, etc! \ No newline at end of file