Repository: creativeprojects/resticprofile (language: python)

Issue:
resticprofile writes log information to stdout when using restic dump
I have been dumping postgresql databases into restic using stdin.  

When I attempt to restore these backups, I am using the restic dump command to send the dump file contents to standard output and piping it to pg_restore as follows:

```bash
resticprofile profile.dump --path /sql.pgdump latest /sql.pgdump | pg_restore -d dbname
```

But the beginning of the stanard output stream includes the following 2 lines generated by resticprofile ahead of the actual dump file contents:

```
2024/03/28 20:11:37 using configuration file: /etc/resticprofile/profiles.toml  
2024/03/28 20:11:37 profile 'db': starting 'dump'                               
PGDM |dbname#14.6 (Ubuntu 14.6-0ubuntu0.22.04.1)#14.6 (Ubuntu 14.6-0ubuntu0.22.0
0ENCODINENCODINGSET client_encoding = 'UTF8';
...
```

pg_restore is complaining about the extraneous output generated by resticprofile.  I [initially reported this issue,](https://github.com/creativeprojects/resticprofile/issues/153#issuecomment-1676495630) but probably not as clearly as shown above.

I propose, when using the restic dump command with resticprofile, either logging is suppressed on standard output entirely, or is instead written to standard error.  

The new run hooks don't really help here when you want to send the dump output to a variety of different commands.  Or in my case, I don't want to accidentally pipe the output directly to a db restore and wipe anything out.  

What do you think?



Discussion / hints:
Since handling stdout properly still needs more time, I agree that we should handle the dump command special in that we don't send anything to stdout other than the command output.

Fix patch:
diff --git a/constants/command.go b/constants/command.go
index 7eabe2c8..ce72a9d5 100644
--- a/constants/command.go
+++ b/constants/command.go
@@ -10,6 +10,7 @@ const (
 	CommandSnapshots = "snapshots"
 	CommandUnlock    = "unlock"
 	CommandMount     = "mount"
+	CommandCat       = "cat"
 	CommandCopy      = "copy"
 	CommandDump      = "dump"
 	CommandFind      = "find"
diff --git a/docs/content/usage/_index.md b/docs/content/usage/_index.md
index 9512f663..1ddc6a92 100644
--- a/docs/content/usage/_index.md
+++ b/docs/content/usage/_index.md
@@ -82,6 +82,7 @@ There are not many options on the command line, most of the options are in the c
 * **[-v | --verbose]**: Force resticprofile and restic to be verbose (override any configuration from the profile)
 * **[--trace]**: Display even more debugging information
 * **[--no-ansi]**: Disable console colouring (to save output into a log file)
+* **[--stderr]**: Send console output from resticprofile to stderr (is enabled for commands `cat` and `dump`)
 * **[--no-lock]**: Disable resticprofile locks, neither create nor fail on a lock. restic locks are unaffected by this option.
 * **[--theme]**: Can be `light`, `dark` or `none`. The colours will adjust to a 
 light or dark terminal (none to disable colouring)
@@ -114,6 +115,7 @@ Most flags for resticprofile can be set using environment variables. If both are
 | `--dry-run`           | `RESTICPROFILE_DRY_RUN`           | `false`          |
 | `--no-lock`           | `RESTICPROFILE_NO_LOCK`           | `false`          |
 | `--lock-wait`         | `RESTICPROFILE_LOCK_WAIT`         | `0`              |
+| `--stderr`            | `RESTICPROFILE_STDERR`            | `false`          |
 | `--no-ansi`           | `RESTICPROFILE_NO_ANSI`           | `false`          |
 | `--theme`             | `RESTICPROFILE_THEME`             | `"light"`        |
 | `--no-priority`       | `RESTICPROFILE_NO_PRIORITY`       | `false`          |
diff --git a/flags.go b/flags.go
index fbabc333..fa999a1f 100644
--- a/flags.go
+++ b/flags.go
@@ -33,6 +33,7 @@ type commandLineFlags struct {
 	resticArgs      []string
 	wait            bool
 	isChild         bool
+	stderr          bool
 	parentPort      int
 	noPriority      bool
 	ignoreOnBattery int
@@ -85,6 +86,7 @@ func loadFlags(args []string) (*pflag.FlagSet, commandLineFlags, error) {
 		dryRun:          envValueOverride(false, "RESTICPROFILE_DRY_RUN"),
 		noLock:          envValueOverride(false, "RESTICPROFILE_NO_LOCK"),
 		lockWait:        envValueOverride(time.Duration(0), "RESTICPROFILE_LOCK_WAIT"),
+		stderr:          envValueOverride(false, "RESTICPROFILE_STDERR"),
 		noAnsi:          envValueOverride(false, "RESTICPROFILE_NO_ANSI"),
 		theme:           envValueOverride(constants.DefaultTheme, "RESTICPROFILE_THEME"),
 		noPriority:      envValueOverride(false, "RESTICPROFILE_NO_PRIORITY"),
@@ -104,6 +106,7 @@ func loadFlags(args []string) (*pflag.FlagSet, commandLineFlags, error) {
 	flagset.BoolVar(&flags.dryRun, "dry-run", flags.dryRun, "display the restic commands instead of running them")
 	flagset.BoolVar(&flags.noLock, "no-lock", flags.noLock, "skip profile lock file")
 	flagset.DurationVar(&flags.lockWait, "lock-wait", flags.lockWait, "wait up to duration to acquire a lock (syntax \"1h5m30s\")")
+	flagset.BoolVar(&flags.stderr, "stderr", flags.noAnsi, "send console output to stderr (enabled for \"cat\" and \"dump\")")
 	flagset.BoolVar(&flags.noAnsi, "no-ansi", flags.noAnsi, "disable ansi control characters (disable console colouring)")
 	flagset.StringVar(&flags.theme, "theme", flags.theme, "console colouring theme (dark, light, none)")
 	flagset.BoolVar(&flags.noPriority, "no-prio", flags.noPriority, "don't change the process priority: used when started from a service that has already set the priority")
diff --git a/logger.go b/logger.go
index 02ce7c37..25c93f0c 100644
--- a/logger.go
+++ b/logger.go
@@ -18,6 +18,7 @@ import (
 	"github.com/creativeprojects/resticprofile/term"
 	"github.com/creativeprojects/resticprofile/util"
 	"github.com/creativeprojects/resticprofile/util/collect"
+	"github.com/fatih/color"
 )
 
 type LogCloser interface {
@@ -26,6 +27,12 @@ type LogCloser interface {
 }
 
 func setupConsoleLogger(flags commandLineFlags) {
+	if flags.stderr {
+		out := color.Output
+		color.Output = color.Error
+		defer func() { color.Output = out }()
+	}
+
 	consoleHandler := clog.NewConsoleHandler("", log.LstdFlags)
 	if flags.theme != "" {
 		consoleHandler.SetTheme(flags.theme)
diff --git a/main.go b/main.go
index f53aa10d..ecd87277 100644
--- a/main.go
+++ b/main.go
@@ -52,7 +52,7 @@ func main() {
 	args := os.Args[1:]
 	_, flags, flagErr := loadFlags(args)
 	if flagErr != nil && flagErr != pflag.ErrHelp {
-		fmt.Println(flagErr)
+		term.Println(flagErr)
 		_ = displayHelpCommand(os.Stdout, commandContext{
 			ownCommands: ownCommands,
 			Context: Context{
@@ -70,7 +70,7 @@ func main() {
 		// keep the console running at the end of the program
 		// so we can see what's going on
 		defer func() {
-			fmt.Println("\n\nPress the Enter Key to continue...")
+			term.Println("\n\nPress the Enter Key to continue...")
 			fmt.Scanln()
 		}()
 	}
@@ -113,6 +113,12 @@ func main() {
 			if ctx != nil {
 				logTarget = ctx.logTarget
 				commandOutput = ctx.commandOutput
+				if ctx.request.command == constants.CommandCat ||
+					ctx.request.command == constants.CommandDump {
+					clog.Debugf("redirecting console to stderr for command %q", ctx.request.command)
+					flags.stderr = true
+				}
+				term.PrintToError = flags.stderr
 			}
 			if logTarget != "" && logTarget != "-" {
 				if closer, err := setupTargetLogger(flags, logTarget, commandOutput); err == nil {
diff --git a/term/term.go b/term/term.go
index ca6c480e..b1c47ff7 100644
--- a/term/term.go
+++ b/term/term.go
@@ -15,6 +15,7 @@ import (
 var (
 	terminalOutput io.Writer = os.Stdout
 	errorOutput    io.Writer = os.Stderr
+	PrintToError             = false
 )
 
 // Flusher allows a Writer to declare it may buffer content that can be flushed
@@ -165,6 +166,9 @@ func FlushAllOutput() {
 // Spaces are added between operands when neither is a string.
 // It returns the number of bytes written and any write error encountered.
 func Print(a ...interface{}) (n int, err error) {
+	if PrintToError {
+		return fmt.Fprint(errorOutput, a...)
+	}
 	return fmt.Fprint(terminalOutput, a...)
 }
 
@@ -172,11 +176,17 @@ func Print(a ...interface{}) (n int, err error) {
 // Spaces are always added between operands and a newline is appended.
 // It returns the number of bytes written and any write error encountered.
 func Println(a ...interface{}) (n int, err error) {
+	if PrintToError {
+		return fmt.Fprintln(errorOutput, a...)
+	}
 	return fmt.Fprintln(terminalOutput, a...)
 }
 
 // Printf formats according to a format specifier and writes to standard output.
 // It returns the number of bytes written and any write error encountered.
 func Printf(format string, a ...interface{}) (n int, err error) {
+	if PrintToError {
+		return fmt.Fprintf(errorOutput, format, a...)
+	}
 	return fmt.Fprintf(terminalOutput, format, a...)
 }
diff --git a/update.go b/update.go
index 96efb9b7..4fd30237 100644
--- a/update.go
+++ b/update.go
@@ -68,7 +68,7 @@ func confirmAndSelfUpdate(quiet, debug bool, version string, prerelease bool) er
 
 	// don't ask in quiet mode
 	if !quiet && !term.AskYesNo(os.Stdin, fmt.Sprintf("Do you want to update to version %s", latest.Version()), true) {
-		fmt.Println("Never mind")
+		term.Println("Never mind")
 		return nil
 	}
 


Test patch:
diff --git a/flags_test.go b/flags_test.go
index 41ca10de..0806ebc6 100644
--- a/flags_test.go
+++ b/flags_test.go
@@ -69,6 +69,7 @@ func TestEnvOverrides(t *testing.T) {
 		dryRun:          setEnv(true, "RESTICPROFILE_DRY_RUN").(bool),
 		noLock:          setEnv(true, "RESTICPROFILE_NO_LOCK").(bool),
 		lockWait:        setEnv(time.Minute*5, "RESTICPROFILE_LOCK_WAIT").(time.Duration),
+		stderr:          setEnv(true, "RESTICPROFILE_STDERR").(bool),
 		noAnsi:          setEnv(true, "RESTICPROFILE_NO_ANSI").(bool),
 		theme:           setEnv("custom-theme", "RESTICPROFILE_THEME").(string),
 		noPriority:      setEnv(true, "RESTICPROFILE_NO_PRIORITY").(bool),