21
dpkg --list |grep linux-image |grep "ii  " | while read line
do
  arr=(${line})
  let i=i+1
  _constr+="${arr[2]} "
done
echo $i
echo ${_constr}

The echo statements outside of the loop do not display the expected variables.

How should I make the contents of the variable propagate outside the loop?

codeforester
  • 39,467
  • 16
  • 112
  • 140
masuch
  • 345
  • 1
  • 2
  • 8

3 Answers3

39

The problem is the pipe, not the loop. Try it this way

let i=0
arr=()
_constr=

while read -r line ; do
    arr=("${line}")
    let i=i+1
    _constr+="${arr[2]} "
done < <(dpkg --list | grep linux-image | grep 'ii  ')

echo "$i"
echo "${_constr}"

Pipes are executed in a subshell, as noted by Blagovest in his comment. Using process substitution instead (this is the < <(commands) syntax) keeps everything in the same process, so changes to global variables are possible.

Incidentally, your pipeline could be improved as well

dpkg --list | grep '^ii.*linux-image'

One less invocation of grep to worry about.

sorpigal
  • 25,504
  • 8
  • 57
  • 75
  • 1
    In bash 3.x, the last line of loop should be like `done <<<\`commands\`` – Ida Oct 24 '12 at 09:08
  • @Ida: What would be the advantage of doing that? Doing that would fail: the subshell would execute and return each result on its own line, which would then be smashed into a single string and fed to `read` at once, which would read until the first embedded newline, loop once and then `while`, having no more input to process, would terminate. This is not what you want. Only the first package version from the query would be in `_constr` afterward. The `<()` construct is understood by bash 3.x. – sorpigal Oct 24 '12 at 20:27
  • +1 + done[space]<[space]<(command) ^ no space above – Yordan Georgiev Jul 19 '16 at 19:12
  • For users reading that. Yes pipe is the origin of the issue, thank you @sorpigal, but when can keep the pipe with `{}` as @gilles-so-stop-being-evil said there https://unix.stackexchange.com/a/9994/372108 with `cat junk | { while read var ; do x=55 ; done echo x=$x }`. It works for me. – phili_b Apr 29 '22 at 13:33
  • @phili_b: that may work in some shells, but it doesn't work in all shells (and not in `bash`). You should avoid relying on it. – sorpigal Apr 29 '22 at 16:54
  • It does work on my ubuntu 20 bash, I finalized a script this afternoon with that. Monday I'll paste its version. – phili_b Apr 29 '22 at 18:06
  • `bash --version` : GNU bash, version 5.0.17(1)-release (x86_64-pc-linux-gnu) Copyright (C) 2019 Free Software Foundation, Inc. – phili_b May 02 '22 at 08:03
  • `seq 4 |{ while read id; do echo "hello: $id"; echo "--"; byeid=$id; done; echo "bye $byeid";}` works with this bash. Be careful at the space before the `while` else we get the error "-bash: syntax error near unexpected token `do'". – phili_b May 04 '22 at 09:57
  • If that's what you mean then that's different. You are still creating a subshell and variables still do not survive outside it, but they exist up to the end of your subshell--which now includes one statement outside the loop. Curly braces are not special here, they just make a larger [compound command](https://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html#tag_001_010_002) – sorpigal May 09 '22 at 22:31
  • Observe: `printf '%s\n' mike robert joe | ( while read i; do printf 'hello %s\n' "$i"; last=$i; done; printf 'goodbye %s\n' "$last" )` using a regular subshell-bonus points, whitespace around the parens are not required. – sorpigal May 09 '22 at 22:43
  • When I found this page and this one of unix stackexchange, It was for concatenate all codes found, by regex, inside each pdf and rename each pdf filename with those codes found, but I can concatenate only when I have a new file. So when I had 45 files, the 45th was not changed, ie your $last or my $byeid. Until I applied the answer of @gilles-so-stop-being-evil, but understanding that the cause was the pipe like you explained. :) – phili_b May 14 '22 at 20:41
  • And I prefere to have my `find` at the left-hand-side, ie before the pipe, and not at the right-hand-side after `< <`. – phili_b May 14 '22 at 20:54
  • My anwser for concatenate all codes found, by regex, inside each pdf and rename each pdf filename with those codes found. https://stackoverflow.com/a/72324790/10489562 – phili_b May 20 '22 at 21:21
3

This somewhat by-passes your question (and it's a good question), but you can achieve the same results using simply:

 _constr=($(dpkg --list | awk '/^ii.*linux-image/{print $2}'))

The ($(cmd)) construct initialises a bash array using the output of the command within.

[me@home]$ echo ${_constr[*]}
linux-image-2.6.35-22-generic linux-image-2.6.35-28-generic linux-image-generic
[me@home]$ echo ${_constr[2]}
linux-image-generic

and you can get the number of elements using ${#_constr[*]}.

[me@home]$ echo ${#_constr[*]}
3
Shawn Chin
  • 84,080
  • 19
  • 162
  • 191
  • This is really the right way to do it. I didn't have a Debian box to test with or I'd have suggested the same thing. – sorpigal Sep 12 '11 at 18:46
2

Alternatively, you can move the echo statements inside the subshell:

dpkg --list |grep linux-image |grep "ii  " | (
  let i=0
  declare -a arr

  while read line
  do
    arr=(${line})
    let i=i+1
    _constr+="${arr[2]} "
  done
  echo $i
  echo ${_constr}
)

Note the insertion of the parenthesis to explicitly define where the subshell begins and ends.

forivall
  • 9,504
  • 2
  • 33
  • 58