Socat option -r,-R path specifications allow use of variables
diff --git a/CHANGES b/CHANGES
index f9d4f05..a1c1a60 100644
--- a/CHANGES
+++ b/CHANGES
@@ -37,6 +37,10 @@
 	Test: RCVTIMEO_DTLS
 	Feature proposed by Vladimir Nikishkin.
 
+	The file names with -r and -R now may contain environment variable
+	references.
+	Test: VARS_IN_SNIFFPATH
+
 Corrections:
 	When a sub process (EXEC, SYSTEM) terminated with exit code other than
 	0, its last sent data might have been lost depending on timing of read/
diff --git a/doc/socat.yo b/doc/socat.yo
index b7689b7..2fdca50 100644
--- a/doc/socat.yo
+++ b/doc/socat.yo
@@ -161,12 +161,16 @@
    Writes the transferred data not only to their target streams, but also to
    stderr. The output format is hexadecimal, prefixed with "> " or "< "
    indicating flow directions. Can be combined with code(-v).
-dit(bf(tt(-r <file>)))
+label(option_r)dit(bf(tt(-r <file>)))
    Dumps the raw (binary) data flowing from left to right address to the given
-   file.
+   file. The file name may contain references to environment variables and
+   code($$) (pid), code($PROGNAME) (see option link(option -lp)(option_lp)),
+   code($TIMESTAMP) (uses format %Y%m%dT%H%M%S), and code(MICROS) (microseconds
+   of daytime). These references have to be protected from shell expansion of
+   course.
 dit(bf(tt(-R <file>)))
    Dumps the raw (binary) data flowing from right to left address to the given
-   file.
+   file. See link(option -r)(option_r) for customization of file name.
 label(option_b)dit(bf(tt(-b))tt(<size>))
    Sets the data transfer block <size> [link(size_t)(TYPE_SIZE_T)].
    At most <size> bytes are transferred per step. Default is 8192 bytes. 
diff --git a/socat.c b/socat.c
index 9f09305..fa39d3b 100644
--- a/socat.c
+++ b/socat.c
@@ -36,8 +36,6 @@
    char logopt;		/* y..syslog; s..stderr; f..file; m..mixed */
    bool lefttoright;	/* first addr ro, second addr wo */
    bool righttoleft;	/* first addr wo, second addr ro */
-   int sniffleft;	/* -1 or an FD for teeing data arriving on xfd1 */
-   int sniffright;	/* -1 or an FD for teeing data arriving on xfd2 */
    xiolock_t lock;	/* a lock file */
    unsigned long log_sigs;	/* signals to be caught just for logging */
 } socat_opts = {
@@ -52,8 +50,6 @@
    's',		/* logopt */
    false,	/* lefttoright */
    false,	/* righttoleft */
-   -1,		/* sniffleft */
-   -1,		/* sniffright */
    { NULL, 0 },	/* lock */
    1<<SIGHUP | 1<<SIGINT | 1<<SIGQUIT | 1<<SIGILL | 1<<SIGABRT | 1<<SIGBUS | 1<<SIGFPE | 1<<SIGSEGV | 1<<SIGTERM, 	/* log_sigs */
 };
@@ -61,7 +57,6 @@
 void socat_usage(FILE *fd);
 void socat_opt_hint(FILE *fd, char a, char b);
 void socat_version(FILE *fd);
-static int xio_openauxwr(const char *path, const char *hint);
 int socat(const char *address1, const char *address2);
 int _socat(void);
 int cv_newline(unsigned char *buff, ssize_t *bytes, int lineterm1, int lineterm2);
@@ -213,9 +208,7 @@
 	       break;
 	    }
 	 }
-	 if ((socat_opts.sniffleft = xio_openauxwr(a, "option -r")) < 0) {
-	    Error2("option -r: failed to open \"%s\": %s", a, strerror(errno));
-	 }
+	 xiosetopt('r', a);
 	 break;
       case 'R': if (arg1[0][2]) {
 	    a = *arg1+2;
@@ -226,9 +219,7 @@
 	       break;
 	    }
 	 }
-	 if ((socat_opts.sniffright = xio_openauxwr(a, "option -R")) < 0) {
-	    Error2("option -R: failed to open \"%s\": %s", a, strerror(errno));
-	 }
+	 xiosetopt('R', a);
 	 break;
       case 'b': if (arg1[0][2]) {
 	    a = *arg1+2;
@@ -655,50 +646,11 @@
 }
 
 
-/* Opens a path for logging or tracing.
-   Return the filedescriptor, or -1 when an error occurred (errn0).
-   Prints messages but no Error().
-*/
-static int xio_openauxwr(const char *path, const char *hint)
-{
-	int fd;
-	int _errno;
-
-	if ((fd = Open(path, O_CREAT|O_WRONLY|O_APPEND|
-#ifdef O_LARGEFILE
-		       O_LARGEFILE|
-#endif
-#ifdef O_CLOEXEC
-		       O_CLOEXEC|
-#endif
-		       O_NONBLOCK, 0664)) < 0) {
-		if (errno != ENXIO)
-			return -1;
-		/* Possibly a named pipe that does not yet have a reader */
-		_errno = errno;
-		Notice3("%s \"%s\": %s, retrying r/w", hint, path, strerror(errno));
-		if ((fd = Open(path, O_CREAT|O_RDWR|O_APPEND|
-#ifdef O_LARGEFILE
-			       O_LARGEFILE|
-#endif
-#ifdef O_CLOEXEC
-			       O_CLOEXEC|
-#endif
-			       O_NONBLOCK, 0664)) < 0) {
-			errno = _errno;
-			return -1;
-		}
-	}
-#ifndef O_CLOEXEC
-	 Fcntl_l(fd, F_SETFD, FD_CLOEXEC);
-#endif
-	 return fd;
-}
-
-
 xiofile_t *sock1, *sock2;
 int closing = 0;	/* 0..no eof yet, 1..first eof just occurred,
 			   2..counting down closing timeout */
+int sniffleft = -1; 		/* -1 or an FD for teeing data arriving on xfd1 */
+int sniffright = -1; 	/* -1 or an FD for teeing data arriving on xfd2 */
 
 /* call this function when the common command line options are parsed, and the
    addresses are extracted (but not resolved). */
@@ -861,6 +813,28 @@
    int wasaction = 1;	/* last poll was active, do NOT sleep before next */
    struct timeval total_timeout;	/* the actual total timeout timer */
 
+   {
+      /* Open sniff file(s) */
+      char name[PATH_MAX];
+      struct timeval tv = { 0 }; 	/* 'cache' to have same time in both */
+
+      if (xioinqopt('r', name, sizeof(name)) == 0) {
+	 if (sniffleft >= 0)  Close(sniffleft);
+	 sniffleft = xio_opensnifffile(name, &tv);
+	 if (sniffleft < 0) {
+	    Error2("option -r \"%s\": %s", name, strerror(errno));
+         }
+      }
+
+      if (xioinqopt('R', name, sizeof(name)) == 0) {
+	 if (sniffright >= 0)  Close(sniffright);
+	 sniffright = xio_opensnifffile(name, &tv);
+	 if (sniffright < 0) {
+	    Error2("option -R \"%s\": %s", name, strerror(errno));
+         }
+      }
+   }
+
 #if WITH_FILAN
    if (socat_opts.debug) {
       int fdi, fdo;
@@ -1398,23 +1372,23 @@
 	       errno = EAGAIN;  return -1;
 	    }
 
-	    if (!righttoleft && socat_opts.sniffleft >= 0) {
-	       if ((sniffed = Write(socat_opts.sniffleft, buff, bytes)) < bytes) {
+	    if (!righttoleft && sniffleft >= 0) {
+	       if ((sniffed = Write(sniffleft, buff, bytes)) < bytes) {
 		  if (sniffed < 0)
 		     Warn3("-r: write(%d, buff, "F_Zu"): %s",
-			   socat_opts.sniffleft, bytes, strerror(errno));
+			   sniffleft, bytes, strerror(errno));
 		  else if (sniffed < bytes)
 		     Warn3("-r: write(%d, buff, "F_Zu") -> "F_Zd,
-			   socat_opts.sniffleft, bytes, sniffed);
+			   sniffleft, bytes, sniffed);
 	       }
-	    } else if (righttoleft && socat_opts.sniffright >= 0) {
-	       if ((sniffed = Write(socat_opts.sniffright, buff, bytes)) < bytes) {
+	    } else if (righttoleft && sniffright >= 0) {
+	       if ((sniffed = Write(sniffright, buff, bytes)) < bytes) {
 		  if (sniffed < 0)
 		     Warn3("-R: write(%d, buff, "F_Zu"): %s",
-			   socat_opts.sniffright, bytes, strerror(errno));
+			   sniffright, bytes, strerror(errno));
 		  else if (sniffed < bytes)
 		     Warn3("-R: write(%d, buff, "F_Zu") -> "F_Zd,
-			   socat_opts.sniffright, bytes, sniffed);
+			   sniffright, bytes, sniffed);
 	       }
 	    }
 
diff --git a/sysutils.c b/sysutils.c
index 90b282f..6c9b1c6 100644
--- a/sysutils.c
+++ b/sysutils.c
@@ -941,3 +941,208 @@
    }
    return res;
 }
+
+
+/* This function gets a string with possible references to environment
+   variables and expands them. Variables must be prefixed with '$' and their
+   names consist only of A-Z, a-z, 0-9, and '_'.
+   The name may be enclosed with { }.
+   To pass a literal "$" us "\$".
+   There are special variables supported whose values do not comr from
+   environment:
+   $$ expands to the process's pid
+   $PROGNAME expands to the executables basename or the value of option -lp
+   $TIMESTAMP expands to the actual time in format %Y%m%dT%H%M%S
+   $MICROS expands to the actual microseconds (decimal)
+   The dst output is a copy of src but with the variables expanded.
+   Returns 0 on success;
+   returns -1 when the output buffer was too short (overflow);
+   returns 1 on syntax error.
+*/
+int expandenv(
+	char *dst, 		/* prealloc'd output buff, will be \0 termd */
+	const char *src, 	/* input string to generate expansion from */
+	size_t n, 		/* length of dst */
+	struct timeval *tv) 	/* in/out timestamp for sync */
+{
+	char c; 		/* char currently being lex'd/parsed */
+	bool esc = false;	/* just got '\' */
+	bool bra = false; 	/* within ${ } */
+	char *nam = NULL; 	/* points to temp.allocated rw copy of src */
+	const char *e;		/* pointer to isolated var name in tmp[] */
+	size_t s=0, d=0; 	/* counters in src, dst */
+	size_t v; 		/* variable name begin */
+	bool ofl = false;	/* dst overflow, output truncated */
+	char tmp[18];		/* buffer for timestamp, micros */
+
+	while (c = src[s++]) {
+		if (esc) {
+			if (c == '\0') {
+				if (d+2 > n)  { ofl = true; break; }
+				dst[d++] = '\\';
+				dst[d++] = c;
+				break;
+			}
+			if (c != '$') {
+				if (d+3 > n)  { ofl = true; break; }
+				dst[d++] = '\\';
+			} else {
+				if (d+2 > n)  { ofl = true; break; }
+			}
+			dst[d++] = c;
+			esc = false;
+			continue;
+		}
+		if (c == '\0') {
+			dst[d++] = c;
+			break;
+		}
+		if (c == '\\') {
+			esc = true;
+			continue;
+		}
+		if (c != '$') {
+			if (d+2 > n)  { ofl = true; break; }
+			dst[d++] = c;
+			continue;
+		}
+
+		/* c == '$': Expecting env var to expand */
+		c = src[s];
+		if (c == '\0') {
+			if (d+2 > n)  { ofl = true; break; }
+			++s;
+			dst[d++] = '$';
+			break;
+		}
+		if (c == '$') {
+			int wr;
+			/* Special case: pid */
+			++s;
+			wr = snprintf(&dst[d], n-d, F_pid, getpid());
+			if (wr >= n-d || wr < 0)  { ofl = true; break; }
+			d += wr;
+			continue;
+		}
+		if (c == '{') {
+			++s;
+			bra = true;
+		}
+		if (!isalpha(c) && c != '_') {
+			/* Special case no var name, just keep '$' */
+			if (d+3 > n)  { ofl = true; break; }
+			++s;
+			dst[d++] = '$';
+			dst[d++] = c;
+			continue;
+		}
+		v = 0; 		/* seems we found valid variable name */
+		if (nam == NULL) {
+			nam = strdup(src+s);
+			if (nam == NULL) {
+				errno = ENOMEM;
+				return -1;
+			}
+		}
+		while (c = src[s]) {
+			if (!isalnum(c) && c != '_') {
+				break;
+			}
+			nam[v++] = c;
+			++s;
+		}
+		if (bra && c != '}') {
+			return 1;
+		}
+
+		nam[v] = '\0';
+		/* Var name is complete */
+		/* Check hardcoded var names */
+		if (strcmp(nam, "PROGNAME") == 0) {
+			e = diag_get_string('p');
+		} else if (strcmp(nam, "TIMESTAMP") == 0) {
+			if (tv->tv_sec == 0) {
+				gettimeofday(tv, NULL);
+			}
+			struct tm tm;
+			localtime_r(&tv->tv_sec, &tm);
+			strftime(tmp, sizeof(tmp), "%Y%m%dT%H%M%S", &tm);
+			e = tmp;
+		} else if (strcmp(nam, "MICROS") == 0) {
+			if (tv->tv_sec == 0) {
+				gettimeofday(tv, NULL);
+			}
+			sprintf(tmp, F_tv_usec, tv->tv_usec);
+			e = tmp;
+		} else {
+			e = getenv(nam);
+		}
+		if (e == NULL) {
+			/* Var not found, skip it */
+			continue;
+		}
+		/* Var found, copy it to output buffer */
+		if (d+strlen(e)+1 > n)  { ofl = true; break; }
+		strcpy(&dst[d], e);
+		d += strlen(&dst[d]);
+		continue;
+	}
+
+	if (nam != NULL) {
+		free(nam);
+	}
+
+	if (ofl) {
+		dst[d] = '\0';
+		return -1;
+	}
+	dst[d] = '\0';
+	if (src[s-1] != '\0') {
+		errno = EINVAL;
+		return -1;
+	}
+	return 0;
+}
+
+int xio_opensnifffile(
+	const char *a,
+	struct timeval *tv)
+{
+	char path[PATH_MAX];
+	int flags;
+	int rc;
+	int fd;
+
+	rc = expandenv(path, a, sizeof(path), tv);
+	if (rc < 0) {
+		Error2("expandenv(source=\"%s\", n="F_Zu"): Out of memory", a, sizeof(path));
+		errno = ENOMEM;
+		return -1;
+	} else if (rc > 0) {
+		Error1("expandenv(source=\"%s\"): Syntax error", a);
+		errno = EINVAL;
+		return -1;
+	}
+	flags = O_CREAT|O_WRONLY|O_APPEND|
+#ifdef O_LARGEFILE
+		O_LARGEFILE|
+#endif
+#ifdef O_CLOEXEC
+		O_CLOEXEC|
+#endif
+		O_NONBLOCK;
+	if ((fd = Open(path, flags, 0664)) < 0) {
+		if (errno == ENXIO) {
+			/* try to open pipe rdwr */
+			if ((fd = Open(path, flags, 0664)) < 0) {
+				return -1;
+			}
+		}
+	}
+#ifdef O_CLOEXEC
+	if (Fcntl_l(fd, F_SETFD, FD_CLOEXEC) < 0) {
+		Warn2("fcntl(%d, F_SETFD, FD_CLOEXEC): %s", fd, strerror(errno));
+	}
+#endif
+	return fd;
+}
diff --git a/sysutils.h b/sysutils.h
index 98c150d..514d1d2 100644
--- a/sysutils.h
+++ b/sysutils.h
@@ -113,5 +113,6 @@
 extern unsigned long int Strtoul(const char *nptr, char **endptr, int base, const char *txt);
 extern long long int Strtoll(const char *nptr, char **endptr, int base, const char *txt);
 extern double Strtod(const char *nptr, char **endptr, const char *txt);
+extern int xio_opensnifffile(const char *a, struct timeval *tv);
 
 #endif /* !defined(__sysutils_h_included) */
diff --git a/test.sh b/test.sh
index b6c378f..edcfcfa 100755
--- a/test.sh
+++ b/test.sh
@@ -15224,6 +15224,7 @@
 fi ;; # NUMCOND, feats
 esac
 PORT=$((PORT+1))
+N=$((N+1))
 
 
 # Test the so-rcvtimeo address option with DTLS
@@ -15297,6 +15298,103 @@
 N=$((N+1))
 
 
+# Test the use of interesting variables in the sniffing file names
+NAME=VARS_IN_SNIFFPATH
+case "$TESTS" in
+*%$N%*|*%functions%*|*%socket%*|*%$NAME%*)
+TEST="$NAME: sniff file names with variables"
+# Start a server process with option fork that sniffs traffic and stores it in
+# two files for each child process, using PID, timestamp, microseconds, and
+# client IP
+# Connect two times.
+# For now we say that the test succeeded when 4 log files have been generated.
+if ! eval $NUMCOND; then :;
+elif ! A=$(testfeats IP4 TCP LISTEN PIPE STDIO); then
+    $PRINTF "test $F_n $TEST... ${YELLOW}Feature $F not available${NORMAL}\n" $N
+    numCANT=$((numCANT+1))
+    listCANT="$listCANT $N"
+elif ! A=$(testaddrs - TCP4 TCP4-LISTEN PIPE STDIO); then
+    $PRINTF "test $F_n $TEST... ${YELLOW}Address $A not available in $SOCAT${NORMAL}\n" $N
+    numCANT=$((numCANT+1))
+    listCANT="$listCANT $N"
+elif ! o=$(testoptions so-reuseaddr fork) >/dev/null; then
+    $PRINTF "test $F_n $TEST... ${YELLOW}Option $o not available in $SOCAT${NORMAL}\n" $N
+    numCANT=$((numCANT+1))
+    listCANT="$listCANT $N"
+elif ! runsip4 >/dev/null; then
+    $PRINTF "test $F_n $TEST... ${YELLOW}IPv4 not available${NORMAL}\n" $N
+    numCANT=$((numCANT+1))
+    listCANT="$listCANT $N"
+else
+tf="$td/test$N.stdout"
+te="$td/test$N.stderr"
+tdiff="$td/test$N.diff"
+da="test$N $(date) $RANDOM"
+newport tcp4 	# or whatever proto, or drop this line
+CMD0="$TRACE $SOCAT $opts -lp server0 -r \"$td/test$N.\\\$PROGNAME-\\\$TIMESTAMP.\\\$MICROS-\\\$SERVER0_PEERADDR-\\\$\\\$.in.log\" -R \"$td/test$N.\\\$PROGNAME-\\\$TIMESTAMP.\\\$MICROS-\\\$SERVER0_PEERADDR-\\\$\\\$.out.log\" TCP4-LISTEN:$PORT,so-reuseaddr,fork PIPE"
+CMD1="$TRACE $SOCAT $opts - TCP4-CONNECT:$LOCALHOST:$PORT"
+printf "test $F_n $TEST... " $N
+eval "$CMD0" >/dev/null 2>"${te}0" &
+pid0=$!
+waittcp4port $PORT 1
+echo "$da" |$CMD1 >"${tf}1a" 2>"${te}1a"
+rc1a=$?
+echo "$da" |$CMD1 >"${tf}1b" 2>"${te}1b"
+rc1b=$?
+kill $(childpids $pid0) $pid0 2>/dev/null; wait
+if [ $rc1a != 0 -o $rc1b != 0 ]; then
+    $PRINTF "$FAILED (client problem)\n"
+    echo "$CMD0 &"
+    cat "${te}0" >&2
+    echo "$CMD1"
+    cat "${te}1a" >&2
+    echo "$CMD1"
+    cat "${te}1b" >&2
+    numFAIL=$((numFAIL+1))
+    listFAIL="$listFAIL $N"
+    namesFAIL="$namesFAIL $NAME"
+elif test $(ls -l $td/test$N.*.log |wc -l) -eq 4 &&
+	test $(ls $td/test$N.*.log |head -n 1 |wc -c) -ge 56; then
+    # Are the names correct?
+    # Convert timestamps to epoch and compare
+    # Are the contents correct?
+    $PRINTF "$OK\n"
+    if [ "$VERBOSE" ]; then echo "$CMD0 &"; fi
+    if [ "$DEBUG" ];   then cat "${te}0" >&2; fi
+    if [ "$VERBOSE" ]; then echo "$CMD1"; fi
+    if [ "$DEBUG" ];   then cat "${te}1a" >&2; fi
+    if [ "$VERBOSE" ]; then echo "$CMD1"; fi
+    if [ "$DEBUG" ];   then cat "${te}1b" >&2; fi
+    numOK=$((numOK+1))
+elif test -f $td/test$N.\$PROGNAME-\$TIMESTAMP.\$MICROS-\$SERVER0_PEERADDR-\$\$.in.log; then
+    $PRINTF "$FAILED (vars not resolved)\n"
+    echo "$CMD0 &"
+    cat "${te}0" >&2
+    echo "$CMD1"
+    cat "${te}1a" >&2
+    echo "$CMD1"
+    cat "${te}1b" >&2
+    numFAIL=$((numFAIL+1))
+    listFAIL="$listFAIL $N"
+    namesFAIL="$namesFAIL $NAME"
+else
+    $PRINTF "$FAILED (unknown)\n"
+    echo "$CMD0 &"
+    cat "${te}0" >&2
+    echo "$CMD1"
+    cat "${te}1a" >&2
+    echo "$CMD1"
+    cat "${te}1b" >&2
+    numFAIL=$((numFAIL+1))
+    listFAIL="$listFAIL $N"
+    namesFAIL="$namesFAIL $NAME"
+fi
+fi # NUMCOND
+ ;;
+esac
+N=$((N+1))
+
+
 # end of common tests
 
 ##################################################################################
diff --git a/utils.c b/utils.c
index 5760831..7243ca9 100644
--- a/utils.c
+++ b/utils.c
@@ -89,7 +89,6 @@
 #endif /* !HAVE_SETENV */
 
 
-
 /* sanitizes an "untrusted" character. output buffer must provide at least 4
    characters space.
    Does not append \0. returns length of output (currently: max 4) */
diff --git a/xio.h b/xio.h
index 4e5bc49..33c2eb4 100644
--- a/xio.h
+++ b/xio.h
@@ -112,6 +112,8 @@
    char preferred_ip;	/* preferred prot.fam. for name resolution ('0' for
 			   unspecified, '4', or '6') */
    bool experimental;	/* enable some features */
+   const char *sniffleft_name; 		/* file name with -r */
+   const char *sniffright_name; 	/* file name with -R */
 } xioparms_t;
 
 /* pack the description of a lock file */
@@ -440,6 +442,8 @@
 
 /* must be outside function for use by childdied handler */
 extern xiofile_t *sock1, *sock2;
+extern int sniffleft, sniffright;
+
 #define NUMUNKNOWN 4
 extern pid_t diedunknown[NUMUNKNOWN];	/* child died before it is registered */
 #define diedunknown1 (diedunknown[0])
diff --git a/xioparam.c b/xioparam.c
index 4c13c0d..5a4d3da 100644
--- a/xioparam.c
+++ b/xioparam.c
@@ -21,7 +21,9 @@
    NULL,	/* syslogfac */
    '4',		/* default_ip */
    '4',		/* preferred_ip */
-   false 	/* experimental */
+   false, 	/* experimental */
+   NULL, 	/* sniffleft_name */
+   NULL 	/* sniffright_name */
 } ;
 
 
@@ -43,6 +45,8 @@
       break;
    case 'l': xioparms.logopt = *arg; break;
    case 'y': xioparms.syslogfac = arg; break;
+   case 'r': xioparms.sniffleft_name = arg; break;
+   case 'R': xioparms.sniffright_name = arg; break;
    default:
       Error2("xiosetopt('%c', \"%s\"): unknown option",
 	     what, arg?arg:"NULL");
@@ -60,6 +64,26 @@
       return 0;
    case 'o': return xioparms.ip4portsep;
    case 'l': return xioparms.logopt;
+   case 'r':
+      if (xioparms.sniffleft_name == NULL) {
+	 return 1;
+      }
+      if (n < strlen(xioparms.sniffleft_name)+1) {
+	 return -1;
+      }
+      arg[0] = '\0';
+      strncat(arg, xioparms.sniffleft_name, n-1);
+      return 0;
+   case 'R':
+      if (xioparms.sniffright_name == NULL) {
+	 return 1;
+      }
+      if (n < strlen(xioparms.sniffright_name)+1) {
+	 return -1;
+      }
+      arg[0] = '\0';
+      strncat(arg, xioparms.sniffright_name, n-1);
+      return 0;
    default:
       Error3("xioinqopt('%c', \"%s\", "F_Zu"): unknown option",
 	     what, arg, n);