17

I have this code that works:

# Hide irrelevant errors so chrome doesn't email us in cron
if [[ $fCron == true ]] ; then
    google-chrome --headless --disable-gpu --dump-dom \
        "$RobWebAddress" > "$DownloadName" 2>/dev/null
else
    # Get silly error messages when running from terminal
    google-chrome --headless --disable-gpu --dump-dom \
        "$RobWebAddress" > "$DownloadName"
fi

If I try to shorten it like this:

# Hide irrelevant errors so chrome doesn't email us in cron
local HideErrors
[[ $fCron == true ]] && HideErrors="2>/dev/null"

google-chrome --headless --disable-gpu --dump-dom \
    "$RobWebAddress" > "$DownloadName" "$HideErrors"

I get error messages:

[0826/043058.634775:ERROR:headless_shell.cc(597)] Open multiple tabs is only supported when remote debugging is enabled.
[0826/043058.672587:ERROR:headless_shell.cc(597)] Open multiple tabs is only supported when remote debugging is enabled.
[0826/043058.711640:ERROR:headless_shell.cc(597)] Open multiple tabs is only supported when remote debugging is enabled.
(... SNIP ...)

Why does a hard-coded argument work but not an argument as a variable?


Edit 2:

Currently I found success with second answer's alternate suggestion:

# Redirect errors when cron is used to /dev/null to reduce emails
ErrorPipe=/dev/stderr
[[ $fCron == true ]] && ErrorPipe=/dev/null

google-chrome --headless --disable-gpu --dump-dom \
                "$RobWebAddress" > "$DownloadName" 2>"$ErrorPipe"

Edit 1:

Based on the first answer, I should point out program header already contains:

[[ $fCron != true ]] &&
    exec 2> >(grep -v 'GtkDialog mapped without a transient parent' >&2)
2

3 Answers 3

22

The reason you can't cause redirection to occur by expanding "$HideErrors" is that symbols like > aren't treated specially after being produced by parameter expansion. This is actually very good, because such symbols appear in text you might want to expand and use literally.

This holds whether or not you quote $HideErrors. The result of parameter expansion is subject to word splitting and globbing when the expansion is unquoted, but that's it.


As for what to do about it, there are numerous ways to achieve conditional redirection. For a very simple command, it may be reasonable write the whole command twice, once in each branch of a case or if-else construct. This soon becomes burdensome, however, and the command you showed is certainly a case where that would not be ideal.

Of the approaches that let you avoid repeating yourself, there are two I especially recommend, because they are quite clean and easy to get right. You'd want to use just one of these, not both at once for the same command and redirection.

Store the command instead of the redirection. Instead of attempting to store the redirection in a variable and applying parameter expansion, store the command in a shell function. Then write a case or if-else, in which the function is called with the redirection on one branch and without it on the other.

If you conceptualize your command as code that you want to write once but run under multiple circumstances, then a function is the natural solution. This is what I usually do. It has the benefit of requiring neither a subshell nor manual storage and resetting of state.

With your code:

launch() {
    google-chrome --headless --disable-gpu --dump-dom \
        "$RobWebAddress" > "$DownloadName"
}

case $fCron in
true)  launch 2>/dev/null;;
*)     launch;; # Get silly error messages when running from terminal
esac

You can apply whatever spacing you like, or if-else instead if you prefer. Note that launch automatically uses the caller's RobWebAddress and DownloadName variables, even if they are local variables, because Bash is dynamically scoped, unlike most programming languages which are lexically scoped.

Run the command in a subshell and conditionally apply the redirection to exec. This is what steeldriver commented about, but inside ( ) to keep the effect local. When the exec builtin is run with no arguments, it doesn't replace the current shell with a new process, but instead applies any of its redirections to the current shell.

(It's also possible to keep track of what standard error was and restore it, without using a subshell and thus without sacrificing the ability to modify the current shell's environment. I'll leave the details of that to other answers, though.)

With your code:

(
    # Suppress silly error messages unless running from terminal
    case $fCron in true) exec 2>/dev/null;; esac

    google-chrome --headless --disable-gpu --dump-dom \
        "$RobWebAddress" > "$DownloadName"
)

After the closing ), standard error is in effect restored to whatever it was before, because it's only really being redirected in the subshell, and not in the parent shell. This, too, works fine with the existing shell variables, since subshells get a copy of those. Although I prefer to use a shell function, I admit this method may require less code.

Both methods work regardless of what file or device standard error starts out as, including in the case of redirections applied to shell functions that call the code that contains the conditional behavior, as well as the case (mentioned in your edit) where standard error for the whole script has already been redirected by a previous exec 2>&fd or exec 2> path. That the path was produced by process substitution is no problem.

8
  • FYI SteelDriver mentioned something about exec don't know if he's planning an answer on that... Aug 27, 2019 at 1:31
  • @WinEunuuchs2Unix I hope such an answer is still posted. Although I mainly recommend using a function, I also included a method involving a redirection on exec. But, as I mentioned parenthetically, I didn't cover more sophisticated applications where the old file descriptor is kept and restored without a subshell. I also didn't cover less sophisticated applications, like just keeping the redirection if this is the end of the script. Another answer, if posted, could cover both, and perhaps more. Aug 27, 2019 at 1:59
  • I've updated my question with an existing exec which shouldn't impact your answer at all I think. Aug 27, 2019 at 2:04
  • @WinEunuuchs2Unix Yes, that should be no problem. I've added a paragraph about it to the end of the answer. Aug 27, 2019 at 2:14
  • Interesting revelation reading your answer, RobWebAddress is definitely global context. DownloadNamewas defined local but should be global context. For some reason child functions inherit local definitions of parents (to wit DownloadName was visible to DownloadAsHTML () function called by UpdateOne () function which defined it as local. It's been a rough day :( Aug 27, 2019 at 2:15
6

Why does a hard-coded argument work but not an argument as a variable?

Because syntax items aren't interpreted from expanded variable values. That is, variable expansion isn't the same as replacing the variable reference with the text of the variable in the command line. (Stuff like ;, |, && and quotes etc. are also not special in the values of variables.)

What you could do is to use aliases, or use the variable to hold just the target of the redirection.

Aliases are just a text replacement, so they can hold syntactic items, like operators and keywords. In a script, you'd need to shopt expand_aliases, since by default they're disabled in non-interactive shells. So, this prints 2 (only):

#!/bin/bash
shopt -s expand_aliases

alias redir='> /dev/null'
redir echo 1
alias redir=''
redir echo 2

(And you could also alias jos=if niin=then soj=fi and then write all your if-statements in Finnish. I'm sure anyone reading the script would love you.)

Alternatively, write the redirection always, but control just the target with a variable. You'll need a no-op target for the case where you don't want to change where the output goes, though but /dev/stderr should work in that case. Actually, adding 2> /dev/stderr isn't a no-op because of the way Linux treats fd's opened from /proc/<pid>/fd as independent from the original. This affects the positioning of the write position and will mess up the output if it goes to a regular file.

It should work in append mode, though (or if stderr goes to a pipe or to a terminal):

#!/bin/sh
exec 2>/tmp/error.log
dst=/dev/null
ls -l /nosuchfile-1 2>> "$dst"     # this doesn't print
dst=/dev/stderr
ls -l /nosuchfile-2 2>> "$dst"
ls -l /nosuchfile-3 2>> "$dst"

So to repeat: 2> /dev/stderr can break.

6
  • Hahaha, I’ll only use Finnish ifs at work from now on. :>
    – dessert
    Aug 27, 2019 at 16:17
  • I like alternate suggestion. The thought of expand_aliases is scary because your program can be held hostage by ~/.bashrc I think. Aug 27, 2019 at 17:20
  • 1
    @WinEunuuchs2Unix, yes, expand_aliases is a bit scary. But ~/.bashrc shouldn't be a problem since it's only read by interactive shells, and .profile and friends that might call it are read only by login shells. Noninteractive non-login shells like scripts shouldn't run any of those. (But then there's $BASH_ENV, and apparently .bashrc is read if stdin is connected to a network socket. How convoluted can it get...)
    – ilkkachu
    Aug 27, 2019 at 17:27
  • Well I understand your alternate suggestion perfectly and I'll try it tonight :) Aug 27, 2019 at 17:29
  • Honestly, I'm not sure which way I'd implement this if I had to. I'd probably store the command in a function or an array and then branch to decide if to put the redirection there (using a function was shown in the other answer). Or that 2>> "$dst" trick, but I just realized it doesn't work in the general case, so better be careful with it.
    – ilkkachu
    Aug 27, 2019 at 17:42
3

Question title: "How to pass 2>/dev/null as a variable?" This can actually be done using eval

joshua@nova:/tmp$ X=">/dev/null"
joshua@nova:/tmp$ echo $X
>/dev/null
joshua@nova:/tmp$ eval echo $X
joshua@nova:/tmp$ eval echo hi
hi
joshua@nova:/tmp$ eval echo hi $X
joshua@nova:/tmp$ echo hi $X
hi >/dev/null
joshua@nova:/tmp$ 

So we can rewrite as

# Hide irrelevant errors so chrome doesn't email us in cron
local HideErrors
local RobWebAddress2
local DownloadName2
[[ $fCron == true ]] && HideErrors="2>/dev/null"
RobWebAddress2='"$RobWebAddress"'
DownloadName2='>"$DownloadName"'

eval google-chrome --headless --disable-gpu --dump-dom \
    $RobWebAddress2 $DownloadName2 "$HideErrors"

Where the indirect variable access prevents expansion from happening too soon on the rest of the command line.

Double quotes in variables work just fine.

joshua@nova:/tmp$ X='"'
joshua@nova:/tmp$ Y='$X'
joshua@nova:/tmp$ eval echo $Y
"
joshua@nova:/tmp$ 
5
  • @EliahKagan: Question title: "How to pass 2>/dev/null as a variable?"
    – Joshua
    Aug 28, 2019 at 15:13
  • Ok it was not working. I have fixed it.
    – Joshua
    Aug 28, 2019 at 15:46
  • Now the file is always named DownloadName--and the literal text RobWebAddress is always used for the URL. You're using $" " quoting. I think this may be unintentional and you may want the $s inside the " ", but you did it that way in both places, so I'm not sure. I think just > "$DownloadName" should fix it. But I understand you may not like that, since accidentally mixing arguments and non-arguments with eval is one reason it's so perilous and widely discouraged to use eval's concatenating behavior. Aug 28, 2019 at 16:03
  • @EliahKagan: Oh. My preferred shell for scripting doesn't have $"" quoting.
    – Joshua
    Aug 28, 2019 at 16:06
  • 1
    If you fix that, it should work. And I was always wrong to think it pasted quotes on arbitrary text! It builds literal arguments that eval concatenates before evaluating. But I think another way to put it is that it's an obfuscated way to write eval 'google-chrome --headless --disable-gpu --dump-dom "$RobWebAddress" > "$DownloadName" '"$HideErrors" that resembles the appearance of the OP's code. And in general, using eval for tasks that don't need it is bad. (None of this excuses--nor even explains--my old reply's wrongness and hostility.) Aug 28, 2019 at 16:29

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .