Date:        Thu, 20 Aug 2026 12:38:26 +0300
    From:        Valery Ushakov <[email protected]>
    Message-ID:  <[email protected]>

  | I thought that would be determined by whether the command we redirect
  | is a "special built-in utility".

I believe you are correct, but I didn't bother to check, and didn't want
to suggest something I wasn't sure would work, and the "in a subshell"
version had no way not to work, hence...   (that's why the "I think" was
in the previous message in this context.)

  | So : > ... would cause the shell to exit on failure, b/c ":" is a
  | speciale built-in,

And it indeed does.

  | but a bare redirection probably doesn't (but I
  | don't have time to find chapter and verse for that at the moment,
  | unfortunately).

As nor did I (and from where I replied (or rather, my access method)
I didn't have the ability to read PDF files, or not easily, so I would
have needed to work it out from tests, or from reading the sh source.

But it turns out you're right, a failing redirect on an otherwise empty
command is just an error (much the same as "command not found") and doesn't
cause exit, but that one I really would have had to check carefully, as the
empty command case is kind of special.

There is a comment in the sh sources that explains it:

                /*
                 * the empty command begins as a normal builtin, and
                 * remains that way while redirects are processed, then
                 * will become special before we get to doing the
                 * var assigns.
                 */

So, any redirect errors on empty commands don't cause exit, but errors
encountered on variable assignments on empty commands, do.

Note that
        var=value
is an empty command, preceded by a var-assign.   That is, a degenerate
case of
        var=value any-cmd arg arg...
where the "any-cmd" (and its args) is missing.   Variable assignments in
sh are not commands, just prefixes to a command.  That is why one can
write
        A=1 B=2 C=3
without semi-colons between then -- you can't write commands that way, as
the words after the command name are its args, not more commands.

  | Hence, the "command" in the original, I guess, to take the edge off :)

In general, yes it would (command reduces the error effects from special
builtins) but not here, for several reasons - first because of the sub-shell
whether the shell exited or not was immaterial - second, printf is definitely
not a special built-in and a redirect failing on an ordinary command certainly
doesn't cause sh to exit.  The actual reason is that (in the script in question,
though not obviously) "printf" is a function, "command printf" prevents the
function being run, and runs the underlying command (built-in or filesystem)
instead.   It is how one redefines a command to do something a little
different than normal, without creating infinite recursion, like

        ls() { command ls -F "$@" ; }

without "command" (or some other way, like calling the internal ls as /bin/ls)
that would simply generate infinite recursion, until the shell runs out of
memory.   Using command there is the right way, it doesn't assume where the
command to be run exists in the filesystem, if it does at all (I use this
technique to, among other things, ensure that "cd" is always run with -P,
not needed with our /bin/sh, but with most others the default is -L instead).

  | (I learned it from dealing with nowadays
  | ubiquitous set -o pipefail in scripts that causes the shell to exit if
  | it doesn't know pipefail).

Yes, "set" is a special built-in, and errors in it would (without a "command"
prefix) cause the shell to exit.   Using "command set -o pipefail" or
testing whether it will work something like

        if (set -o pipefail) 2>/dev/null
        then
                PFon='set -o pipefail'
                PFoff='set +o pipefail'
        else
                PFon= PFoff=
        fi

and then just using $PFon and $PFoff (unquoted definitely) as commands,
when you want to turn pipefail on/off, would also work, and be cheaper
on shells where pipefail is not supported.

But then again, if you ever need to use the pipefail option turned on,
you probably should make sure not to use a shell which doesn't support
that - if you don't need it, don't turn it on "just because", it isn't
that sort of option, whether to have it on or not needs to be decided for
each pipeline in the source, for some you want it, for most, you really
don't (which is why the world survived for so long before the option was
invented.)

Jason: what all this means is that you can avoid the subshell, and the fork
it entails, by just using:

        if 2>/dev/null >/etc/rcorder.cache.tmp ; then

but as this is a one off, executed only when saving the cache results,
one extra fork would barely make a differnce I suspect.   The redirects
need to be in that order to work as intended.

And last:

[email protected] said:

  | I would have expected that to be
  |     if (:> /etc/rcorder.cache.tmp) 2>/dev/null; then
  | and am suprised the shell accepts redirection on an empty command. 

sh doesn't allow completely empty commands, like "; ;" or "{ ; }"
but as long as there is something, anything at all, in the command,
it is OK.   That can be an actual command, a var-assign (without a
command, or with one), or a redirect, with or without a command, or
a var-assign, or both.   A redirect without an associated command simply
performs the redirect for its side effects (such as verifying it works,
creating a file if it is > or >>, discarding a here-doc with <<word, etc)

Further, as suggested above with the PFon PFoff example even something like

        CMD=
        { $CMD ; }

is OK, even though after $CMD is expanded, there is nothing at all left,
not even an empty string - that would require { "$CMD" ; } and that one
would fail (at execution time).

You're just not allowed to write nothing at all in a place where a command
is expected, which is why (before the ! reserved word prefix was invented)
it was necessary to write something like

        if [ something ] ; then : ; else commands... ; fi

to execute commands... when the something test fails, as there has
to be some form of non-empty command between the then and else -- using
an empty $CMD would also work, as would a meaningless do-nothing
non-failing redirect: eg: "</dev/null" which opens /dev/null and immediately
closes it again; but is not recommended, that actually does real work,
unlike : or $CMD (where CMD='') which do (almost) nothing at all.

kre

ps: I am attaching a simple test script, which tries cases which work,
and which fail, and prints the status.  There are 12 tests, the first 6
are for a file which (normally anyway) can be created, the second 6 for
a file which (normally anyway) cannot.   The order the tests are run is
deliberate, as you will see - the final test doesn't ever get to run the
command which prints the exit status, the shell exited already - were that
one not the last intended to be run, it would be the last run anyway!

If you use bash (or yash) to run the script, then you won't see that effect,
as by default, they blur the special/non-special built-in distiction, and
treat all built-ins the same.  With bash using "bash -o posix" to run it
brings back that distinction, then it behaves the same as our sh (for this).
Yash would do the same, except its "-o posix" doesn't just mean "do what
posix says we must", it also means "do nothing posix doesn't say we can",
and the A++ in the $(( )) arith in the script isn't required by posix, just
permitted as an extension, so that barfs with "yash -o posix".  If you
try the script with our /bin/ksh (or even pdksh, a slightly more modern
version) you will encounter one if its bugs.  Most other shells (ksh93,
mksh, bosh, dash) run the script just the same way sh does, with no need
for extra args to make it happen.   Don't bother trying with non-posix
conforming shells (like zsh, or anything even more exotic, or csh obviously).

Ignore the use of "eval", that changes nothing, just allows a lot of
repeated text to be avoided.  It would not be used when using one of these
techniques in a normal script, just the string being eval'd (which is
printed to stdout) literally in the script.

That is, except the eval (for simplicity anyway) required me to use
printf ""  rather than the printf '' I would normally use, not that it
makes any difference at all which quote style is used to form an empty string.
I just like 'quoting' for printf format strings, as it guarantees I don't
accidentally slip an expansion in there (or rather, prevents the expansion
working when I do) and means I don't need to even think about whether to use
one \ or two of them, in there, or use of (intended literal) '$', ...

#! /bin/sh

trap "printf '\n'" EXIT

A=0
for f in /tmp/anything /no/such/file
do
	for C in '( >"$f" ) 2>/dev/null'	\
		'2>/dev/null  >"$f"'		\
		'printf "" 2>/dev/null >"$f"'	\
		'command : 2>/dev/null >"$f"'	\
		'(: >"$f") 2>/dev/null'	\
		': 2>/dev/null >"$f"'
	do
		printf '%2d: f=%-14s; %-30s: ' "$((++A))" "$f" "$C"
		eval "$C"
		printf '%2d: %d\n' "$A" "$?"
	done

done

Reply via email to