48

Currently I have a shell script which logs messages to a log file like this:

log_file="/some/dir/log_file.log"
echo "some text" >> $log_file
do_some_command
echo "more text" >> $log_file
do_other_command

When executing this script, there is no output to screen, and, since I'm connecting to the server via putty, I have to open another connection and do "tail -f log_file_path.log", because I can't terminate the running script and I want to see the output in real time.

Obviously, what I want is that the text messages are printed on screen and into file, but I'd like to do it in one line, not two lines, one of which has no redirection to file.

How to achieve this?

69

This works:

command | tee -a "$log_file"

tee saves input to a file (use -a to append rather than overwrite), and copies the input to standard output as well.

7

You can use a here-doc and . source it for an efficient, POSIX-friendly general collector model.

. 8<<-\IOHERE /proc/self/fd/8

command
… 
fn() { declaration ; } <<WHATEVER
# though a nested heredoc might be finicky
# about stdin depending on shell
WHATEVER
cat -u ./stdout | ./works.as >> expect.ed
IOHERE

When you open the heredoc you signal the shell with an IOHERE input token that it should redirect its input to the file-descriptor you specify until it encounters the other end of your limiter token. I've looked around but I haven't seen many examples of the use of the redirection fd number as I've shown above in combination with the heredoc operator, though its usage is clearly specified in the POSIX basic shell-command guidelines. Most people just point it at stdin and shoot, but I find sourcing scriptlets this way can keep stdin free and the constituent apps from complaining about blocked I/o paths.

The heredoc's contents are streamed to the file descriptor you specify, which is in turn then interpreted as shell-code and executed by the . builtin, but not without specifying a specific path for . . If the /proc/self path gives you trouble try /dev/fd/n or /proc/$$. This same method works on pipes, by the way:

cat ./*.sh | . /dev/stdin

Is probably at least as unwise as it looks. You can do the same with sh, of course, but .'s purpose is to execute in the current shell environment, which is probably what you want, and, depending on your shell, is a lot more likely to work with a heredoc than with a standard anonymous pipe.

Anyway, as you've probably noticed, I still haven't answered your question. But if you think about it, in the same way the heredoc streams all of your code to .'s in, it also provides you a single, simple, outpoint:

. 5<<EOIN /dev/fd/5 |\ 
    tee -a ./log.file | cat -u >$(tty)
script
… 
more script
EOIN

So all of the terminal stdout from any of the code executed in your heredoc is piped out from . as a matter of course and can easily be tee'd off of a single pipe. I included the unbuffered cat call because I'm unclear about the current stdout direction, but its probably redundant (almost certainly it is as written anyway) and the pipeline can probably end right at tee.

You might also question the missing backslash quote in the second example. This part is important to understand before you jump-in and might give you few ideas about how it can be used. A quoted heredoc limiter (so far we've used IOHERE and EOIN, and the first I quoted with a backslash, though 'single' or "double" quotes would serve the same purpose) will bar the shell from performing any parameter expansion on the contents, but an unquoted limiter will leave its contents open to expansion. The consequences of this when your heredoc is . sourced are dramatic:

. 3<<HD ${fdpath}/3
: \${vars=$(printf '${var%s=$((%s*2))},' `seq 1 100`)} 
HD
echo $vars
> 4,8,12… 
echo $var1 $var51
> 4 104

Because I didn't quote the heredoc limiter the shell expanded the contents as it read it in and before serving the resulting file descriptor to . to execute. This essentially resulted in the commands being parsed twice - the expandable ones anyway. Because I backslash quoted the $vars parameter expansion the shell ignored its declaration on the first pass and only stripped the backslash so the whole printf expanded contents could be evaluated by null when . sourced the script on the second pass.

This functionality is basically exactly what the dangerous eval shell builtin can provide, even if the quoting is much easier to handle in a heredoc than with eval, and can be equally as dangerous. Unless you plan it carefully it is probably best to quote the "EOF" limiter as a matter of habit. Just saying.

EDIT: Eh, I'm looking back at this and thinking it's a little too much of a stretch. If ALL you need to do is concatenate several outputs into one pipe then the simplest method is just to use a:

{ or ( command ) list ; } | tee -a ea.sy >>pea.sy

The curlies will attempt to run contents in the current shell whereas the parens will sub-out automatically. Still, anyone can tell you that and, at least in my opinion, the . heredoc solution is much more valuable information, especially if you'd like to understand how the shell actually works.

Have fun!

3

I found this answer while looking to modify an install script to write an install log.

My script is already full of echo statements like:

echo "Found OLD run script $oldrunscriptname"
echo "Please run OLD tmunsetup script first!"

And I didn't want a tee statement to run it (or another script to call the existing one with a tee), so I wrote this:

#!/bin/bash
# A Shell subroutine to echo to screen and a log file

LOGFILE="junklog"

echolog()
(
echo $1
echo $1 >> $LOGFILE
)


echo "Going"
echolog "tojunk"

# eof

So now in my original script, I can just change 'echo' to 'echolog' where I want the output in a log file.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.