Blob


1 In my [previous article](/2019-10-16-poor-mans-unbound-dashboard.html)
2 I used a bash(1) construct to send the same data to two different places.
4 The fact that I was using a bash-specific (more or less) construct really
5 bothered me this time. So I thought if there is a way in sh(1) to take
6 a piece of data from a pipeline and send it to two different places.
8 The canonical way to do this is `tee(1)`, and it's what I've used it the linked article:
10 ```sh
11 something | tee >(cmd) | something-else
12 ```
14 (the `>(cmd)` construct is bash, and zsh I think, specific: it executes
15 `cmd` in a subshell and execute `tee` with something like `/dev/fd/63`
16 -- at least on linux and OpenBSD.)
18 Now I'll try to describe three different approaches to emulate the same
19 behavior of `>(...)` in pure sh(1), trying to outline both the advantages
20 and the cons.
22 ## temporary files
24 That is:
26 ```sh
27 something | tee a-file | something-else
28 cmd < a-file
30 # and then, probably
31 rm a-file
32 ```
34 (or even better with `mktemp(1)`)
36 This is the most simple approach I can think of. The obvious disadvantage
37 is the need to create and delete files quickly. This shouldn't be a huge
38 problem in most cases, but for script do this constantly and (possibly)
39 quickly it can be. Plus, at least for me, it doesn't seem very elegant,
40 but it's subjective.
42 ## fifo to the rescue
44 An almost similar approach is to use fifo. A fifo, aka a named pipe,
45 is a special file that you can write to and read from, in a FIFO
46 fashion.
48 ```sh
49 mkfifo a-fifo
50 something | tee a-fifo | something-else
51 while read line < a-fifo; do echo $line; done | cmd
52 ```
54 This is especially useful if you need to have a long-running script that
55 needs to duplicate a stream from one source. You create the fifos at the
56 startup and then you're done, instead of constantly creating temporary
57 files over and over again. An annoying thing is that you need to wrap
58 the read from the fifo in some sort of loop: AFAIK to read the next item
59 in a fifo you need to close and re-open it.
61 ## An unexpected sed
63 Let's add another requisite: other than dumbly copy the stream, suppose
64 that you want also to apply some sort of transformation to one branch
65 of the duplication, like filter out something.
67 Well, you can adapt the last two examples by adding a `| sed '...'`.
68 Or you can use the `w` command of sed.
70 I'll be a bit more specific: let's say that you need to *dispatch*
71 something taken from a single stream and feed *n* subprocess. It's
72 exactly what I've tried to achieve in my last post: I've used `>(cmd)`
73 to dispatch the unbound statistics to different fifo, and then I had
74 different processes pulling out those stats from the fifo to draw graphs.
76 The code was like
78 ```sh
79 unbound-control stats \
80 | tee >(grep something > a-fifo) \
81 | tee >(grep something-else > b-fifo) \
82 | ...
83 ```
85 Well, that can be rewritten as
87 ```sh
88 unbound-control stats \
89 | sed 's/something/&/w a-fifo' \
90 | sed 's/something-else/&/w b-fifo \
91 | ...
92 ```
94 by leveraging some sed behaviors:
96 1. every line read is printed to stdout (eventually transformed)
97 2. after the `s` command you can use `w` to write the transformed lines
98 to a local file
100 I've got this *illumination* by reading [Sed - An introduction and
101 Tutorial by Bruce Barnett](http://www.grymoire.com/Unix/Sed.html).
103 I tried to use `sed 'g/something/w a-fifo'` but `sed` complained
104 about "extra characters after command", so I assume that the no-op
105 `s/something/&/` is needed.
107 -----
109 Edit: I tried with `g/...` because I thought that `sed` had a `g` command
110 that behaves like the `vi(1)` or `sam(1)` `g` command. `sed` has a `g`
111 command, but does something different. The correct form would be `sed
112 '/something/w a-fifo'` because `sed`, like `awk(1)`, has the ability to
113 perform commands only on lines that match a regular expression.
115 -----
117 I find this last technique pretty, even prettier than the `>(grep ... >)`
118 idiom I used previously since it avoids the subshell to do the filtering
119 and because it does not make assumption on the shell used -- it only
120 requires a POSIX shell.
122 I hope you found this interesting. At least to me it really is.