Some remarks and useful tricks
Introduction
From What does " 2>&1 " mean?, I will use in this answer the command:
ls -ld /tmp /tnt
for populating simultaneously both STDIN
and STDERR
. (In the hope you don't have any entry in your root, called tnt
.)
Redirections from script himself
You could plan redirections from the script itself:
#!/bin/bash
exec 1>>logfile.txt
exec 2>&1
/bin/ls -ld /tmp /tnt
Running this will create/append logfile.txt
, containing:
/bin/ls: cannot access '/tnt': No such file or directory
drwxrwxrwt 2 root root 4096 Apr 5 11:20 /tmp
Or
#!/bin/bash
exec 1>>logfile.txt
exec 2>>errfile.txt
/bin/ls -ld /tmp /tnt
While create or append standard output to logfile.txt
and create or append errors output to errfile.txt
.
Log to many different files
You could create two different logfiles, appending to one overall log and recreating another last log:
#!/bin/bash
if [ -e lastlog.txt ] ;then
mv -f lastlog.txt lastlog.old
fi
exec 1> >(tee -a overall.log /dev/tty >lastlog.txt)
exec 2>&1
ls -ld /tnt /tmp
Running this script will
- if
lastlog.txt
already exist, rename them to lastlog.old
(overwriting lastlog.old
if they exist).
- create a new
lastlog.txt
.
- append everything to
overall.log
- output everything to the terminal.
Simple and combined logs
#!/bin/bash
[ -e lastlog.txt ] && mv -f lastlog.txt lastlog.old
[ -e lasterr.txt ] && mv -f lasterr.txt lasterr.old
exec 1> >(tee -a overall.log combined.log /dev/tty >lastlog.txt)
exec 2> >(tee -a overall.err combined.log /dev/tty >lasterr.txt)
ls -ld /tnt /tmp
So you have
lastlog.txt
last run log file
lasterr.txt
last run error file
lastlog.old
previous run log file
lasterr.old
previous run error file
overall.log
appended overall log file
overall.err
appended overall error file
combined.log
appended overall error and log combined file.
- still output to the terminal
Same, using timestamped log filenames:
#!/bin/bash
sTime=${EPOCHSECONDS}
for pre in log err; do
printf -v ${pre}file '%s-%(%Y%m%d%H%M%S)T-%08x.txt' "$pre" "$sTime" $$
done
exec 1> >(tee -a overall.log combined.log /dev/tty >"$logfile")
exec 2> >(tee -a overall.err combined.log /dev/tty >"$errfile")
ls -ld /t{nt,mp}
- I use
$sTime
in order to ensure both file will present exactly same timestamp. (If $EPOCHSECONDS
is invoked two times, they could differ between each expansion!)
- I add formed 8 characters hexadecimal representation of currend pid:
$$
in order to ensure unicity.
after 3rd run, you must found 9 files:
ls -ltr
-rw-r--r-- 1 user user 49 19 nov 10:36 log-20231119103611-00120649.txt
-rw-r--r-- 1 user user 73 19 nov 10:36 err-20231119103611-00120649.txt
-rw-r--r-- 1 user user 73 19 nov 10:36 err-20231119103634-001207b8.txt
-rw-r--r-- 1 user user 49 19 nov 10:36 log-20231119103634-001207b8.txt
-rw-r--r-- 1 user user 147 19 nov 10:40 overall.log
-rw-r--r-- 1 user user 219 19 nov 10:40 overall.err
-rw-r--r-- 1 user user 49 19 nov 10:40 log-20231119104000-001216f0.txt
-rw-r--r-- 1 user user 73 19 nov 10:40 err-20231119104000-001216f0.txt
-rw-r--r-- 1 user user 366 19 nov 10:40 combined.log
And for interactive session, use stdbuf
:
Regarding Fonic' comment and after some test, I have to agree: with tee
, stdbuf
is useless. But ...
If you plan to use this in *interactive* shell, you must tell `tee` to not buffering his input/output:
# Source this to multi-log your session
[ -e lasterr.txt ] && mv -f lasterr.txt lasterr.old
[ -e lastlog.txt ] && mv -f lastlog.txt lastlog.old
exec 2> >(exec stdbuf -i0 -o0 tee -a overall.err combined.log /dev/tty >lasterr.txt)
exec 1> >(exec stdbuf -i0 -o0 tee -a overall.log combined.log /dev/tty >lastlog.txt)
Once sourced this, you could try:
ls -ld /tnt /tmp
More complex sample
From my 3 remarks about how to Convert Unix timestamp to a date string
I've used more complex command to parse and reassemble squid
's log in real time: As each line begin by an UNIX EPOCH with milliseconds, I split the line on 1st dot, add @
symbol before EPOCH SECONDS to
pass them to date -f - +%F\ %T
then reassemble date
's output and the rest of line with a dot by using paste -d .
.
exec {datesfd}<> <(:)
tail -f /var/log/squid/access.log |
tee >(
exec sed -u 's/^\([0-9]\+\)\..*/@\1/'|
stdbuf -o0 date -f - +%F\ %T >&$datesfd
) |
sed -u 's/^[0-9]\+\.//' |
paste -d . /dev/fd/$datesfd -
With date
, stdbuf
was required...
Some explanations about exec
and stdbuf
commands:
Running forks
by using $(...)
or <(...)
is done by running subshell wich will execute binaries in another subshell (subsubshell). The exec
command tell shell that there are not further command in script to be run, so binary (stdbuf ... tee
) will be executed as replacement process, at same level (no need to reserve more memory for running another sub-process).
From bash
's man page (man -P'less +/^\ *exec\ ' bash
):
exec [-cl] [-a name] [command [arguments]]
If command is specified, it replaces the
shell. No new process is created....
This is not really needed, but reduce system footprint.
From stdbuf
's man page:
NAME
stdbuf - Run COMMAND, with modified buffering
operations for its standard streams.
This will tell system to use unbuffered I/O for tee
command. So all outputs will be updated immediately, when some input are coming.