简体   繁体   中英

STDOUT and STDERR to SAME file and STDERR to ANOTHER [checklist]

UPDATE

Thanks a lot for the answers and comments. Thanks to @Fravadona for the help I managed to copy it and change a few things. This is the actual code:

{ exec 0>&9; } 1> /dev/null 2>&1 || exec 9> /dev/tty

checklist() {

    (( $# >= 1 )) || return 1

    {
      printf '%s' "$2"
      #(( $# > 1 )) #&& printf ' %q' "${@:2}" I think I don't need this line
      printf '...\n'
    } >&9

    if "$1"; then
      printf '%s\n' "$3" >&9
    else
      printf '%s\n' "$4" >&9
    fi 
 }

checklist "ping -c 3 google.es" "Trying to ping google" "Ping is successfull!" "Error trying to ping google!"
checklist lsblk "Trying to list disks" "The command ended succesfully!" "Error trying to list disks!"

The first argument is the actual command, the 2nd is the string I want to show while the command is running, the 3rd is when the command ends successfully and the 4th is when there's some error.

The output I'm getting is:

Trying to ping google...
Error trying to ping google!
Trying to list disks...
The command ended succesfully!

The lsblk command is working because there's only 1 word but the ping command doesn't work because it has more words. How can I do it? I've tried with single quotes, backward apostrophe, brackets... I think I'm missing something.

The logs are working fine.

Thank you for your help!

ORIGINAL

I'm doing an script in bash and I want to redirect the stderr and stdout to a single file (output.log) and the stderr to another one (error.log). In the terminal, it has to show only the echo commands I want from the script.

I just want to make like a checklist. Redirect stdout and stderr from the commands to a file and stderr to diferent file. Once you have this, the only thing left is checking if the command is successful or not. This is easy just checking the stderr file.

I did something similar to what I want but I have to execute the script like this:

{ . script.sh 2>&1 1>&3 | tee error.log; } > output.log 3>&1

But in each command that I want to show in the terminal, I have to add:

| tee -a /dev/tty

A minor problem with this method is that tput civis and tput cnorm to hide and show the cursor is not working

It would be nice if I can execute the script like this, but this is not required as long as tput works:

. script.sh

I'm showing an example with ping command and another one to show you what I want:

echo "Trying ping..." | tee -a /dev/tty # Show to terminal
if ! ping -c 3 google.es; then # Check if the ping is successful or not, redirecting all outputs to files
  echo "Error trying to ping" | tee -a /dev/tty # Show to terminal
else
  echo "Ping finished successfully!" | tee -a /dev/tty # Show to terminal
fi

echo "Trying {cmd}..." | tee -a /dev/tty # Show to terminal
if ! {cmd2}; then # Check if the {cmd2} is successful or not, redirecting all outputs to files
  echo "Error trying to {cmd2}..." | tee -a /dev/tty # Show to terminal
else
  echo "{cmd2} finished successfully!" | tee -a /dev/tty # Show to terminal
fi
.
.
.
.

The output would be and I want is:

Trying ping...
Ping finished successfully!
Trying {cmd2}...
Error trying to {cmd2}!
.
.
.
.

If there's another way to make that checklist I am all ears.

Thank you for your time:)

PS: I will do functions to refactor the code, don't worry about that. For example a function to check if the command is successful or not.

UPDATE

If you could refactor script.sh like this:

#!/bin/bash

{ exec 0>&9; } 1> /dev/null 2>&1 || exec 9> /dev/tty

checklist() {

    (( $# >= 1 )) || return 1

    {
        printf 'Trying `%q' "$1"
        (( $# > 1 )) && printf ' %q' "${@:2}"
        printf '`...\n'
    } >&9

    if "$@"
    then
        printf '%s finished successfully!\n' "$1" >&9
    else
        printf 'Error with %s\n' "$1" >&9
    fi 
 }

checklist ping -c 3 google.es
checklist {cmd2}

Then, when you run it without sourcing:

{ ./script.sh 2>&1 1>&3 | tee error.log; } > output.log 3>&1

On the terminal you'll get:

Trying `ping -c 3 google.es`...
ping finished successfully!
Trying `\{cmd2\}`...
Error with {cmd2}

In error.log you'll have:

./script.sh: line 15: {cmd2}: command not found

In output.log there'll be:

PING google.es (142.250.75.227): 56 data bytes
64 bytes from 142.250.75.227: icmp_seq=0 ttl=115 time=1.743 ms
64 bytes from 142.250.75.227: icmp_seq=1 ttl=115 time=1.893 ms
64 bytes from 142.250.75.227: icmp_seq=2 ttl=115 time=1.569 ms

--- google.es ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 1.569/1.735/1.893/0.132 ms
./script.sh: line 15: {cmd2}: command not found

OLD ANSWER

A standard way would be:

 { . script.sh 2>&1 1>&3 | tee error.log; } > output.log 3>&1

To understand it you need to know that the "redirections" of a command are executed from right to left .

Let's consider what happens with cmd 2>&1 1>&3 .

  1. The content of 1 is "moved" to 3 .

  2. The content 2 is "moved" to 1 .

  3. As there is no more redirections for cmd , the contents of 1 , 2 (empty) and 3 are then "consumed" by the rest of the script.

Now, what would happen if we change the order of the redirections with cmd 1>&3 2>&1 ?

  1. The content of 2 is "moved" to 1

  2. The content of 1 (which also contains a copy of 2 ) is then "moved" to 3 .

  3. As there is no more redirections for cmd , the contents of the file descriptors 1 (empty), 2 (empty) and 3 are then "consumed" by the rest of the script.

This should do what you want:

. script.sh > >( tee output.log ) 2> >( tee error.log )

This uses process substitutions to write the output of the command to two instances of tee .

The first substitution writes stdout to output.log and copies it to the existing stdout (likely your terminal). The second substitution copies stderr to both error.log and stdout, which goes to the first tee that writes it to the output file and the terminal.

I think this will do what you want.

$: { ls -ld . bogus 2> >( tee err.log ) ;} >all.log
$: grep . ???.log
all.log:drwxr-xr-x 1 paul 1049089 0 Jan  9 11:45 .
all.log:ls: cannot access 'bogus': No such file or directory
err.log:ls: cannot access 'bogus': No such file or directory

or to show the results as you go,

$: { ls -ld . bogus 2> >( tee err.log ) ;} | tee all.log
drwxr-xr-x 1 paul 1049089 0 Jan  9 12:01 .
ls: cannot access 'bogus': No such file or directory
$: grep . ???.log
all.log:drwxr-xr-x 1 paul 1049089 0 Jan  9 12:01 .
all.log:ls: cannot access 'bogus': No such file or directory
err.log:ls: cannot access 'bogus': No such file or directory

It's also possible to do this inside your script, though it starts getting messy...

$: cat tst
#!/bin/bash
{ { # double-group all I/O
    ls -ld . bogus
} 2> >( tee err.log ) ;} | tee all.log # split by grouping

$: ./tst
ls: cannot access 'bogus': No such file or directory
drwxr-xr-x 1 paul 1049089 0 Jan  9 12:09 .

$: grep . ???.log
all.log:ls: cannot access 'bogus': No such file or directory
all.log:drwxr-xr-x 1 paul 1049089 0 Jan  9 12:09 .
err.log:ls: cannot access 'bogus': No such file or directory

You could also do something like -

#!/bin/bash
exec > >(tee all.log)
{ # group all I/O
  ls -ld . bogus
} 2> >( tee err.log )

though at that point I think the collation of stdout (which is buffered) and stderr (which is generally not) will decouple. Logs with errors out of sync can be nightnarish to debug...

addendum

how can I select to show the commands I want?

I usually use a function.

show() { echo "$@" | tee -a /dev/tty; }

This will still send a copy through STDOUT to the global logging, but will fork a copy to the terminal as well.

ls -ld . bogus # . listing to stdout, "bogus: not found" to stderr
show this goes to terminal # also stdout's log...

If you need to make it able to accept input on stdin instead of args -

show() { 
  if (($#))
  then echo "$@"
  else cat
  fi | tee -a /dev/tty
}

$: show this to tty >x.log
this to tty

$: cat x.log
this to tty

$: ls -ld . bogus | show >x.log # doesn't catch stderr
ls: cannot access 'bogus': No such file or directory
drwxr-xr-x 1 paul 1049089 0 Jan 10 14:04 .

$: cat x.log
drwxr-xr-x 1 paul 1049089 0 Jan 10 14:04 .

Then yes, you have to explicitly use show instead of echo` for things you DEFINITELY want on your console, but you have options. It will still work with the above code though.

wrapping up -

if what you want is ONLY things you specify going to the console, try something like this -

$: cat tst
#! /bin/bash
exec 1> all.log          # all stdout to all.log
exec 2> >( tee err.log ) # stderr->err.log, tee'd to stdout->all.log
show() { if (($#)); then echo "$@"; else cat; fi | tee -a /dev/tty; }
# now just write your code, and use the show function for console output
ls -ld . bogus
show logged AND to screen.

$: ./tst
logged AND to screen.

$: cat err.log
ls: cannot access 'bogus': No such file or directory

$: cat all.log
ls: cannot access 'bogus': No such file or directory
drwxr-xr-x 1 paul 1049089 0 Jan 10 14:15 .
logged AND to screen.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM