0
#!/bin/bash
FOO=1
{ FOO=2; echo "a"; }
echo $FOO  # prints 2
{ FOO=3; echo "b"; } | wc
echo $FOO  # prints 3

How come the assignment FOO=2 is visible to the rest of the script, but the assignment FOO=3 isn't? What's special about the pipe into wc that hides it?

And what's the neatest idiom for getting environment variables out of that group, while retaining the pipe?

Restriction: I'm not allowed to mess with the command that I'm piping into. That's a third-party tool processes its input and delivers its output in way that I can't alter. (I've written wc here only to make a minimal repro).

Here are some related answers, but none of them address this problem:

Real-world scenario

My actual scenario is that I'm testing an interactive process called an "LSP server", the kind of thing that powers autocomplete+hover+gotodef inside editors like VSCode and Atom. The interactive process reads commands from stdin, and writes responses to stdout. Everyone else has been writing complicated test harnesses in typescript or python or java. But I believe bash will let me write simpler test harnesses:

{
  echo "command1"
  <wait for response to appear in /tmp/log>
  echo "command2"
  <wait for response to appear in /tmp/log>
} | lsp_server > /tmp/log

All this is working fine. But now I want to record timings:

{
  T1=$(date +%s)
  echo "command1"
  <wait for response to appear in /tmp/log>
  T2=$(date +%s)
  echo "command2"
  <wait for response to appear in /tmp/log>
  T3=$(date +%s)
} | lsp_server > /tmp/log
echo "command1: $(( T2 - T1 )) seconds"
echo "command2: $(( T3 - T2 )) seconds"

This is running into the problem that I described.

For now I'm solving it by creating a temporary file and then sourcing it:

{
  echo "T1=$(date +%s)" > /tmp/vars
  echo "command1"
  <wait for response to appear in /tmp/log>
  echo "T2=$(date +%s)" >> /tmp/vars
  echo "command2"
  <wait for response to appear in /tmp/log>
  echo "T3=$(date +%s)" >> /tmp/vars
} | lsp_server > /tmp/log
. /tmp/vars
echo "command1: $(( T2 - T1 )) seconds"
echo "command2: $(( T3 - T2 )) seconds"

But the idea of sourcing the file seems really crummy. I think there must be something I'm missing.

Lucian Wischik
  • 2,160
  • 1
  • 20
  • 25
  • This is [BashFAQ #24](https://mywiki.wooledge.org/BashFAQ/024). Basically, pipeline components run in subshells, with their own variable scope. For what you're trying to do here, use a coproc instead (or just open a FD writing to `lsp_server` via a process substitution). – Charles Duffy Dec 13 '19 at 01:05
  • 1
    `exec {lsp_write_fd}> >(lsp_server >/tmp/log); echo "command1" >&"$lsp_write_fd"; ...; exec {lsp_write_fd}>&-`, assuming bash 4.3 or newer, is one easy way to avoid that pipeline (and the subshells that go with it). – Charles Duffy Dec 13 '19 at 01:07
  • BTW, it's a lot more efficient to `exec {vars_fd}>/tmp/vars` just once at the top of your script, and then redirect to `>&"$vars_fd"` when you want to write to it, instead of re-opening the file over and over every time you want to append a line. – Charles Duffy Dec 13 '19 at 01:26
  • BTW, again assuming a recent release of bash, consider `printf 'T1=%(%s)T\n' -1` as a much faster/more efficient replacement to `date -s`; don't need the time needed to start the external `date` command throwing off your weightings. – Charles Duffy Dec 13 '19 at 17:13
  • @CharlesDuffy I don't know the official way to thank people on stackoverflow, but MANY SINCERE THANKS for your prompt and thorough answer. Much appreciated. – Lucian Wischik Dec 17 '19 at 01:20

0 Answers0