Store multiple values in a single variable in linux - bash

I need to store multiple command outputs that comes from a for loop into a single variable.
The variable should store the output separated by space.
Output that I am expecting:
for i in a b c d e
do
xyz=$i
done
echo $xyz should return a b c d e

Concatenate strings
#!/usr/bin/env sh
# initialize xyz to empty
xyz=
for i in a b c d e
do
# concatenate xyz space and i into xyz
xyz="$xyz $i"
done
# remove the extra leading space from xyz
xyz="${xyz# }"
echo "$xyz should return a b c d e"
Or with growing the arguments array:
#!/usr/bin/env sh
# clear arguments array
set --
for i in a b c d e
do
# add i to arguments array
set -- "$#" "$i"
done
# expand arguments into xyz
xyz="$*"
echo "$xyz should return a b c d e"

I'd suggest to use an array instead of var
for i in a b c d e
do
arr+=("$i")
done
echo "${arr[#]}"

Here is one way to solve the problem:
for i in a b c d e
do
xyz="$xyz$sep$i"
sep=" "
done
echo "$xyz"
Here is the output:
a b c d e
Here is how it works. At first, both variables xyz and sep are unset, so expanding them (that is, $xyz and $sep) leads to empty strings. The sep variable represents a separator which is empty initially.
After the first iteration, the xyz variable is set to a and the sep variable is set to (a space). Now for the second and subsequent iterations, the separator is a space, so now xyz="$xyz$sep$i" appends a space and the new value of $i to the existing value of $xyz. For example, in the second iteration, xyz="$xyz$sep$i" expands to xyz="a b".
Of course, another alternative that avoids altering the separator value between iterations is as follows:
for i in a b c d e
do
xyz="$xyz $i"
done
xyz="${xyz# }"
echo "$xyz"
Once again the output is:
a b c d e
In this alternative solution, the separator is always a space. But that means that in the first iteration of xyz="$xyz$sep$i" adds a leading space to $xyz, that is, after the first iteration, the value of the xyz variable is a (note the leading space). The final value of the variable thus becomes a b c d e (note the leading space again). We get rid of this leading space with the ${xyz# } syntax which removes the smallest prefix pattern.
See POSIX.1-2008: Shell Command Language: 2.6.2 Parameter Expansion for more details on parameter expansion and prefix pattern removal.

Related

why there is different output in for-loop

Linux bash: why the two shell script as follow had different result?
[root#yumserver ~]# data="a,b,c";IFS=",";for i in $data;do echo $i;done
a
b
c
[root#yumserver ~]# IFS=",";for i in a,b,c;do echo $i;done
a b c
expect output: the second script also output:
a
b
c
I should understood what #M.NejatAydin means。Thanks also #EdMorton,#HaimCohen!
[root#k8smaster01 ~]# set -x;data="a,b,c";IFS=",";echo $data;echo "$data";for i in $data;do echo $i;done
+ data=a,b,c
+ IFS=,
+ echo a b c
a b c
+ echo a,b,c
a,b,c
+ for i in '$data'
+ echo a
a
+ for i in '$data'
+ echo b
b
+ for i in '$data'
+ echo c
c
[root#k8smaster01 ~]# IFS=",";for i in a,b,c;do echo $i;done
+ IFS=,
+ for i in a,b,c
+ echo a b c
a b c
Word splitting is performed on the results of unquoted expansions (specifically, parameter expansions, command substitutions, and arithmetic expansions, with a few exceptions which are not relevant here). The literal string a,b,c in the
second for loop is not an expansion at all. Thus, word splitting is not performed on that literal string. But note that, in the second example, word splitting is still performed on $i (an unquoted expansion) in the command echo $i.
It seems the point of confusion is where and when the IFS is used. It is used in the word splitting phase following an (unquoted) expansion. It is not used when the shell reads its input and breaks the input into words, which is an earlier phase.
Note: IFS is also used in other contexts (eg, by the read builtin command) which are not relevant to this question.
#HaimCohen explained in detail why you get a different result with those two approaches. Which is what you asked. His answer is correct, it should get upvoted and accepted.
Just a trivial addition from my side: you can easily modify the second of your approaches however if you define the variable on the fly:
IFS=",";for i in ${var="a,b,c"};do echo $i;done

Parse filename string and extract parent at specific level using shell

I have a filename as a string, say filname="a/b/c/d.png".
Is there a general method to extract the parent directory at a given level using ONLY shell parameter expansion?
I.e. I would like to extract "level 1" and return c or "level 2" and return b.
Explicitly, I DO NOT want to get the entire parent path (i.e. a/b/c/, which is the result of ${filename%/*}).
Using just shell parameter expansion, assuming bash, you can first transform the path into an array (splitting on /) and then ask for specific array indexes:
filename=a/b/c/d.png
IFS=/
filename_array=( $filename )
unset IFS
echo "0 = ${filename_array[0]}"
echo "1 = ${filename_array[1]}"
echo "2 = ${filename_array[2]}"
echo "3 = ${filename_array[3]}"
Running the above produces:
0 = a
1 = b
2 = c
3 = d.png
These indexes are the reverse of what you want, but a little
arithmetic should fix that.
Using zsh, the :h modifier trims the final component off a path in variable expansion.
The (s:...:) parameter expansion flag can be used to split the contents of a variable. Combine those with normal array indexing where a negative index goes from the end of the array, and...
$ filename=a/b/c/d.png
$ print $filename:h
a/b/c
$ level=1
$ print ${${(s:/:)filename:h}[-level]}
c
$ level=2
$ print ${${(s:/:)filename:h}[-level]}
b
You could also use array subscript flags instead to avoid the nested expansion:
$ level=1
$ print ${filename[(ws:/:)-level-1]}
c
$ level=2
$ print ${filename[(ws:/:)-level-1]}
b
w makes the index of a scalar split on words instead of by character, and s:...: has the same meaning, to say what to split on. Have to subtract one from the level to skip over the trailing d.png, since it's not stripped off already like the first way.
The :h (head) and :t (tail) expansion modifiers in zsh accept digits to specify a level; they can be combined to get a subset of the path:
> filname="a/b/c/d.png"
> print ${filname:t2}
c/d.png
> print ${filname:t2:h1}
c
> print ${filname:t3:h1}
b
If the level is in a variable, then the F modifier can be used to repeat the h modifier a specific number of times:
> for i in 1 2 3; printf '%s: %s\n' $i ${filname:F(i)h:t}
1: c
2: b
3: a
If using printf (a shell builtin) is allowed then this will do the trick in bash:
filename='a/b/c/d.png'
level=2
printf -v spaces '%*s' $level
pattern=${spaces//?/'/*'}
component=${filename%$pattern}
component=${component##*/}
echo $component
prints out
b
You can assign different values to the variable level.

How can I create array of lines in this case?

Given a file so that in any line can be more than one word, and exists a single space between any word to other, for example:
a a a a
b b b b
c c
d d
a a a a
How can I create array so that in the cell number i will be the line number i , but WITHOUT DUPLICATES BETWEEN THE ELEMENTS IN THE ARRAY !
In according to the file above, we will need create this array:
Array[0]="a a a a" , Array[1]="b b b b" , Array[2]="c c" , Array[3]=d d.
(The name of the file pass to the script as argument).
I know how to create array that will contain all the lines. Something like that:
Array=()
while read line; do
Array=("${Array[#]}" "${line}")
done < $1
But how can I pass to the while read.. the sorting (and uniq) output of the file?
You should be able to use done < <(sort "$1" | uniq) in place of done < $1.
The <() syntax creates a file-like object from a subshell to execute a separate set of commands.

Fish: Iterate over a string

How would you iterate over a string in the fish shell?
Unfortunately iterating over a string isn't as straight forward as iterating over a list:
↪ for c in a b c
echo $c
end
a
b
c
↪ for c in abc
echo $c
end
abc
The for loop in fish operates on a list.
for VARNAME in [VALUES...]; COMMANDS...; end
The builtin string command (since v2.3.0) can be used to split a string into a list of characters.
↪ string split '' abc
a
b
c
The output is a list, so array operations will work.
↪ for c in (string split '' abc)
echo $c
end
a
b
c
A more complex example iterating over the string with an index.
↪ set --local chars (string split '' abc)
for i in (seq (count $chars))
echo $i: $chars[$i]
end
1: a
2: b
3: c

Filter input to remove certain characters/strings

I have quick question about text parsing, for example:
INPUT="a b c d e f g"
PATTERN="a e g"
INPUT variable should be modified so that PATTERN characters should be removed, so in this example:
OUTPUT="b c d f"
I've tried to use tr -d $x in a for loop counting by 'PATTERN' but I don't know how to pass output for the next loop iteration.
edit:
How if a INPUT and PATTERN variables contain strings instead of single characters???
Where does $x come from? Anyway, you were close:
tr -d "$PATTERN" <<< $INPUT
To assign the result to a variable, just use
OUTPUT=$(tr -d "$PATTERN" <<< $INPUT)
Just note that spaces will be removed, too, because they are part of the $PATTERN.
Pure Bash using parameter substitution:
INPUT="a b c d e f g"
PATTERN="a e g"
for p in $PATTERN; do
INPUT=${INPUT/ $p/}
INPUT=${INPUT/$p /}
done
echo "'$INPUT'"
Result:
'b c d f'

Resources