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.

5
  • This is BashFAQ #24. 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). Commented Dec 13, 2019 at 1: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). Commented Dec 13, 2019 at 1: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. Commented Dec 13, 2019 at 1: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. Commented Dec 13, 2019 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. Commented Dec 17, 2019 at 1:20

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.