Contents:

1. Introduction

This port scheme allows creating a "filter" port that works like a "tee"; that is, it lets data pass through it, but it also copies it into another port. This basically allows creating a "bifurcation", often called a "tee" (because of the shape of the letter T). This is very useful in combination with the chain:// scheme or the pipe function, as it allows creating complex filtering graphs for your data stream.

2. Overview

We define the tee:// scheme in the usual way; although this is not really a network protocol, REBOL does not like it if we don't use net-install. The specified port id does not matter (any random number will do).

Since we're writing a non pass-thru port, we only have to define read and write, and REBOL will map insert, copy, pick etc. to them internally; REBOL will also do any line termination conversion, however we recommend only using /binary ports for filtering. This scheme has not been tested without /binary.

Overview

net-utils/net-install 'tee make Root-Protocol [
 init: func [port spec] [
  Initialize the tee port port from spec
 ]
 open: func [port] [
  Open the tee port port
 ]
 close: func [port] [
  Close the tee port port
 ]
 write: func [port data /local write's locals] [
  Write data into the buffer and to port/sub-port
 ]
 read: func [port data /local read's locals] [
  Put the contents of the buffer into data and clear the buffer
 ]
 update: func [port] [
  Update port/sub-port
 ]
] 80

3. Usage examples

The following example will create a port that lets all data pass through unaltered, but also copies and inserts it into my-port.

Usage examples

tee: open/binary [scheme: 'tee sub-port: my-port]

If you just want to copy to a file, you can specify it directly or use a url! like in the example below and the file will be opened for writing and closed automatically for you.

Usage examples +≡

tee: open/binary [scheme: 'tee sub-port: %/path/to/file]
tee: open/binary tee:/path/to/file

4. Implementation

The implementation of the port scheme is relatively trivial.

4.1 Initialize the tee port port from spec

The init function is used to initialize the port (called when you make it). The user will neet to pass a port to write to in the sub-port field, or specify a file. (See Usage examples.)

Initialize the tee port port from spec

port/url: spec
if url? spec [
 ; assume that user wants to write to a file
 spec: to file! skip spec 4
 if find/match spec %// [remove spec]
 port/sub-port: spec
]
if none? port/sub-port [
 net-error "You must specify a sub port to write to"
]

4.2 Open the tee port port

The open function creates a buffer to store data into and, if a file! was provided, opens it in /binary/direct/write/new mode. We also force the /direct mode, since we are a "filter" kind of port.

Open the tee port port

port/locals: context [
 buffer: make binary! 1024
 close?: no
]
if file? port/sub-port [
 port/locals/close?: yes
 port/sub-port: system/words/open/binary/direct/write/new port/sub-port
]
port/state/flags: port/state/flags or system/standard/port-flags/direct

4.3 Close the tee port port

On close, we need to close the file port if we had opened it; we also set port/locals to none to allow REBOL to reclaim memory.

Close the tee port port

if port/locals/close? [
 system/words/close port/sub-port
 ; allow reopening it easily
 port/sub-port: join port/sub-port/path port/sub-port/target
]
port/locals: none

4.4 Write data into the buffer and to port/sub-port

On write, we write the data into the sub port, and also store it into our buffer.

Note: we have a workaround here for a bug with checksum:// ports. We'll remove this code once the bug has been fixed.

Write data into the buffer and to port/sub-port

set/any 'len write-io port/sub-port data port/state/num
; checksum:// will return unset, so we always assume all data has been written
unless value? 'len [len: port/state/num]
insert/part tail port/locals/buffer data len
len

4.4.1 write's locals

write's locals

len

4.5 Put the contents of the buffer into data and clear the buffer

On read, we copy data from our buffer into data. We remove the copied data from port/locals/buffer and return the actual number of bytes copied.

Put the contents of the buffer into data and clear the buffer

len: min length? port/locals/buffer port/state/num
insert/part tail data port/locals/buffer len
remove/part port/locals/buffer len
len

4.5.1 read's locals

read's locals

len

4.6 Update port/sub-port

On update, we just call update on the sub port (we're using attempt because file ports don't like update).

Update port/sub-port

attempt [system/words/update port/sub-port]