KSH Split String Issues - ksh

I have a string in a variable Var.
And the value looks like this:
Var="Key1:Val1~Key2:Val2~"
I just simply need this split by "~" and assigned to an array in KSH only
When I try Var2=$(echo $Var | sed $'s/~/\\n/g')
and check the size of Var2 array as follows:
ArrSize=${#Var2[#]}
I always get 1. I would have imagined that would be 2. Please Help

Assuming you want to use the x=( list of array items ) method of populating the array then you need to wrap the right side of the assignment in a pair of parens, eg:
$ Var2=( $( echo $Var | sed $'s/~/\\n/g' ) )
$ typeset -p Var2
typeset -a Var2=(Key1:Val1 Key2:Val2)
$ echo "${#Var2[#]}"
2
Other options that accomplish the same thing but reduce the overhead of subprocess calls:
here string:
$ Var2=( $(sed 's/~/ /g' <<< "${Var}") )
$ typeset -p Var2
typeset -a Var2=(Key1:Val1 Key2:Val2)
$ echo "${#Var2[#]}"
2
parameter substitution:
$ Var2=( ${Var//\~/ } )
$ typeset -p Var2
typeset -a Var2=(Key1:Val1 Key2:Val2)
$ echo "${#Var2[#]}"
2
NOTE: while ${var//~/ } works in ksh, other shells (eg, bash) require the ~ to be escaped (ie, \~); ksh appears to work with both - ~ and \~ = so I've updated the answer to include the escape

Related

bash string split by delimiter breaks on empty value

I have a variable like LINE=foo,bar,,baz
I tried to split this with delimiter , using 2 different techniques:
array=(${LINE//,/ })
array=($(echo "$LINE" | tr ',' '\n'))
echo ${array[2]} should return an empty value but it returns baz
(Both treat baz as the 3rd value when it should be the 4th instead.)
You can do this with read -a and an alternate delimiter:
IFS=, read -a array <<<"$LINE"
Note that since the assignment to IFS is a prefix to the read command, it only applies to that one command, and you don't have to set it back to normal afterward. Also, unlike the ones that depend on word-splitting of unquoted variables, it won't try to "expand" any entries that look like filename wildcards into lists of matching files.
Demo:
$ LINE=foo,bar,,baz
$ IFS=, read -a array <<<"$LINE"
$ declare -p array
declare -a array='([0]="foo" [1]="bar" [2]="" [3]="baz")'
You are relying on a sequence of whitespace between tokens as the separator, but of course, that will lose any empty strings.
As a somewhat crude workaround, temporarily override IFS:
oldIFS=$IFS
IFS=','
array=($LINE)
IFS=$oldIFS
Demo: https://ideone.com/Dd1gUV
By default array treat all 'blank' characters as delimiters. If you need to preserve empty values you have to take care of them first.
line='foo, ,bar,,baz'
line=${line//,,/,null,}
line=${line//,[[:blank:]],/,null,}
array=(${line//,/ })
$ echo "${array[#]}"
foo null bar null baz
$ echo "${array[#]//null/ }"
foo bar baz
You could use mapfile (or readarray, same thing):
$ LINE=foo,bar,,baz
$ declare -a PARTS
$ mapfile -t -d, PARTS <<<"$LINE"
$ declare -p PARTS
declare -a PARTS=([0]="foo" [1]="bar" [2]="" [3]=$'baz\n')
There's an extraneous newline at the end of the 3rd element, hence the $'baz\n' value, so you'd have to handle that (see discussion in comments, though). Not sure where it comes from.

Bash Associative Array from String?

A command emits the string: "[abc]=kjlkjkl [def]=yutuiu [ghi]=jljlkj"
I want to load a bash associative array using these key|value pairs, but the result I'm getting is a single row array where the key is formed of the first pair [abc]=kjlkjkl and the value is the whole of the rest of the string, so: declare -p arr returns declare -A arr["[abc]=kjlkjkl"]="[def]=yutuiu [ghi]=jljlkj"
This is what I am doing at the moment. Where am I going wrong please?
declare -A arr=()
while read -r a b; do
arr["$a"]="$b"
done < <(command that outputs the string "[abc]=kjlkjkl [def]=yutuiu [ghi]=jljlkj")
You need to parse it: split the string on spaces, split each key-value pair on the equals sign, and get rid of the brackets.
Here's one way, using tr to replace the spaces with newlines, then tr again to remove all brackets (including any that occur in a value), then IFS="=" to split the key-value pairs. I'm sure this could be done more effectively, like with AWK or Perl, but I don't know how.
declare -A arr=()
while IFS="=" read -r a b; do
arr["$a"]="$b"
done < <(
echo "[abc]=kjlkjkl [def]=yutuiu [ghi]=jljlkj" |
tr ' ' '\n' |
tr -d '[]'
)
echo "${arr[def]}" # -> yutuiu
See Cyrus's answer for another take on this, with the space and equals steps combined.
Append this to your command which outputs the string:
| tr ' =' '\n ' | tr -d '[]'
You can use the "eval declare" trick - but be sure your input is clean.
#! /bin/bash
s='[abc]=kjlkjkl [def]=yutuiu [ghi]=jljlkj'
eval declare -A arr=("$s")
echo ${arr[def]} # yutuiu
If the input is insecure, don't use it. Imagine (don't try) what would happen if
s='); rm -rf / #'
The "proper" good™ solution would be to write your own parser and tokenize the input. For example read the input char by char, handle [ and ] and = and space and optionally quoting. After parsing the string, assign the output to an associative array.
A simple way could be:
echo "[abc]=kjlkjkl [def]=yutuiu [ghi]=jljlkj" |
xargs -n1 |
{
declare -A arr;
while IFS= read -r line; do
if [[ "$line" =~ ^\[([a-z]*)\]=([a-z]*)$ ]]; then
arr[${BASH_REMATCH[1]}]=${BASH_REMATCH[2]}
fi
done
declare -p arr
}
outputs:
declare -A arr=([abc]="kjlkjkl" [ghi]="jljlkj" [def]="yutuiu" )

How do I populate a bash associative array with command output?

I'm trying to populate an associative array with the output of a command. I can do it without a command as:
$ declare -A x=( [first]=foo [second]=bar )
$ echo "${x[first]}, ${x[second]}"
foo, bar
and I can populate a non-associative array with command output as:
$ declare y=( $(echo 'foo bar') )
$ echo "${y[0]}, ${y[1]}"
foo, bar
but when I try to build on both of the above to create a statement that will populate an associative array from a command, I get the following error message:
$ declare -A z=( $(echo '[first]=foo [second]=bar') )
-bash: z: $(echo '[first]=foo [second]=bar'): must use subscript when assigning associative array
Why am I getting that error message and what is the correct syntax to populate an associative array with the output of a command? I am trying to avoid using eval for the usual reasons, do not want to use a temp file, and of course echo is just being used as an example of a command that produces the effect in question, the real command will be more complicated.
So, based on a couple of the answers below, it looks like it was just my quoting that was a problem:
$ declare -A z="( $(echo '[first]=foo [second]=bar') )"
$ echo "${z[first]}, ${z[second]}"
foo, bar
and with spaces in the indices and values:
$ declare -A z="( $(echo '[first field]="foo with space" [second]="space bar"') )"
$ echo "${z[first field]}, ${z[second]}"
foo with space, space bar
EDIT in response to a question in the comments about why the quotes are necessary (How do I populate a bash associative array with command output?) - I don't exactly know but maybe someone else can explain using the results of this script as reference (not expecting the specified indices to be used in the indexed arrays, they're just part of the strings being populated as the array values):
$ cat tst.sh
#!/bin/env bash
set -x
printf 'Indexed, no quotes\n'
declare -a w=( $(echo '[first]=foo [second]=bar') )
declare -p w
printf '\n---\n'
printf 'Indexed, with quotes\n'
declare -a x="( $(echo '[first]=foo [second]=bar') )"
declare -p x
printf '\n---\n'
printf 'Associative, no quotes\n'
declare -A y="( $(echo '[first]=foo [second]=bar') )"
declare -p y
printf '\n---\n'
printf 'Associative, with quotes\n'
declare -A z=( $(echo '[first]=foo [second]=bar') )
declare -p z
.
$ ./tst.sh
+ printf 'Indexed, no quotes\n'
Indexed, no quotes
+ w=($(echo '[first]=foo [second]=bar'))
++ echo '[first]=foo [second]=bar'
+ declare -a w
+ declare -p w
declare -a w=([0]="[first]=foo" [1]="[second]=bar")
+ printf '\n---\n'
---
+ printf 'Indexed, with quotes\n'
Indexed, with quotes
++ echo '[first]=foo [second]=bar'
+ declare -a 'x=( [first]=foo [second]=bar )'
+ declare -p x
declare -a x=([0]="bar")
+ printf '\n---\n'
---
+ printf 'Associative, no quotes\n'
Associative, no quotes
++ echo '[first]=foo [second]=bar'
+ declare -A 'y=( [first]=foo [second]=bar )'
+ declare -p y
declare -A y=([second]="bar" [first]="foo" )
+ printf '\n---\n'
---
+ printf 'Associative, with quotes\n'
Associative, with quotes
+ z=($(echo '[first]=foo [second]=bar'))
./tst.sh: line 24: z: $(echo '[first]=foo [second]=bar'): must use subscript when assigning associative array
+ declare -A z
+ declare -p z
declare -A z=()
Here is a traditional while loop approach to populate an associative array from a command's output:
while IFS= read -r; do
declare -A z+="( $REPLY )"
done < <(printf '[first]=foo [second]=bar\n[third]=baz\n')
# check output
$> echo "${z[first]}, ${z[second]}, ${z[third]}"
foo, bar, baz
# or declare -p
$> declare -p z
declare -A z='([third]="baz" [second]="bar" [first]="foo" )'
EDIT: Your original attempt will also work with proper quotes:
$> unset z
$> declare -A z="( $(echo '[first]=foo [second]=bar') )"
$> declare -p z
declare -A z='([second]="bar" [first]="foo" )'
I imagine this is somewhat brittle, but you can make the entire z=(...) assignment the result of a command substitution.
declare -A "$(echo z="($(echo '[first]=foo [second]=bar'))")"
Given that this works:
declare -A z=([first]=$(echo 'foo') [second]=$(echo 'bar'))
I'm guessing that Bash needs to see the associative array initialization list before doing any substitutions. So I don't see a way to avoid eval:
eval "declare -A z=($(echo '[first]=foo [second]=bar'))"
What is a "usual reason" to avoid eval?

How do you split a string from shell-redirect or `read`?

I'm trying to split key value pairs (around an = sign) which I then use to edit a config file, using bash. But I need an alternative to the <<< syntax for IFS.
The below works on my host system, but when i log in to my ubuntu virtual machine through ssh I have the wrong bash version. Whatever I try, <<< fails. (I am definitely calling the right version of bash at the top of the file, using #!/bin/bash (and I've tried #!/bin/sh etc too)).
I know I can use IFS as follows on my host mac os x system:
var="word=hello"
IFS='=' read -a array <<<"$var"
echo ${array[0]} ${array[1]]}
#alternative -for calling through e.g. sh file.sh param=value
for var in "$#"
do
IFS='=' read -a array <<<"$var"
echo ${array[0]} ${array[1]]}
done
#alternative
IFS='=' read -ra array <<< "a=b"
declare -p array
echo ${array[0]} ${array[1]}
But this doesn't work on my vm.
I also know that I can should be able to switch the <<< syntax through backticks, $() or echo "$var" | ... but I can't get it to work - as follows:
#Fails
IFS='=' read -ra myarray -d '' <"$var"
echo ${array[0]} ${array[1]]}
#Fails
echo "$var" | IFS='=' read -a array
echo ${array[0]} ${array[1]]}
#fails
echo "a=b" | IFS='=' read -a array
declare -p array
echo ${array[0]} ${array[1]}
Grateful for any pointers as I'm really new to bash.
Your first failed attempt is because < and <<< are different operators. < opens the named file.
The second fails because read only sets the value of array in the subshell started by the pipe; that shell exits after the completion of the pipe, and array disappears with it.
The third fails for the same reason as the second; the declare that follows doesn't make any difference.
Your attempts have been confounded because you have to use the variable in the same sub-shell as read.
$ echo 'foo=bar' | { IFS='=' read -a array; echo ${array[0]}; }
foo
And if you want your variable durable (ie, outside the sub-shell scope):
$ var=$(echo 'foo=bar' | { IFS='=' read -a array; echo ${array[0]}; })
$ echo $var
foo
Clearly, it isn't pretty.
Update: If -a is missing, that suggests you're out of the land of arrays. You can try parameter substitution:
str='foo=bar'
var=${str%=*}
val=${str#*=}
And if that doesn't work, fall back to good ole cut:
str='foo=bar'
var=$(echo $str | cut -f 1 -d =)
val=$(echo $str | cut -f 2 -d =)

How to count number of words from String using shell

I want to count number of words from a String using Shell.
Suppose the String is:
input="Count from this String"
Here the delimiter is space ' ' and expected output is 4.
There can also be trailing space characters in the input string like "Count from this String ".
If there are trailing space in the String, it should produce the same output, that is 4. How can I do this?
echo "$input" | wc -w
Use wc -w to count the number of words.
Or as per dogbane's suggestion, the echo can be got rid of as well:
wc -w <<< "$input"
If <<< is not supported by your shell you can try this variant:
wc -w << END_OF_INPUT
$input
END_OF_INPUT
You don't need an external command like wc because you can do it in pure bash which is more efficient.
Convert the string into an array and then count the elements in the array:
$ input="Count from this String "
$ words=( $input )
$ echo ${#words[#]}
4
Alternatively, use set to set positional parameters and then count them:
$ input="Count from this String "
$ set -- $input
$ echo $#
4
Try the following one-liner:
echo $(c() { echo $#; }; c $input)
It basically defines c() function and passes $input as the argument, then $# returns number of elements in the argument separated by whitespace. To change the delimiter, you may change IFS (a special variable).
To do it in pure bash avoiding side-effects, do it in a sub-shell:
$ input="Count from this string "
$ echo $(IFS=' '; set -f -- $input; echo $#)
4
It works with other separators as well:
$ input="dog,cat,snake,billy goat,horse"
$ echo $(IFS=,; set -f -- $input; echo $#)
5
$ echo $(IFS=' '; set -f -- $input; echo $#)
2
Note the use of "set -f" which disables bash filename expansion in the subshell, so if the caller wants expansion it should be done beforehand (Hat Tip #mkelement0).
echo "$input" | awk '{print NF}'
function count_item() {
return $#
}
input="one two three"
count_item $input
n=$?
echo $n
NOTE: function parameter passing treat space as separated
argument, therefore $# works. $? is the return value
of the recently called function.
I'll just chime in with a perl one-liner (avoiding 'useless use of echo'):
perl -lane 'print scalar(#F)' <<< $input
It is efficient external command free way, like #dogbane's. But it works correctly with stars.
$ input="Count from *"
$ IFS=" " read -r -a words <<< "${input}"
$ echo ${#words[#]}
3
If input="Count from *" then words=( $input ) will invoke glob expansion. So size of words array will vary depending on count of files in current directory. So we use IFS=" " read -r -a words <<< "${input}" instead it.
see https://github.com/koalaman/shellcheck/wiki/SC2206

Resources