lmi
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [lmi] Shell scripting bafflement


From: Vadim Zeitlin
Subject: Re: [lmi] Shell scripting bafflement
Date: Thu, 27 Feb 2020 03:38:48 +0100

On Thu, 27 Feb 2020 00:34:01 +0000 Greg Chicares <address@hidden> wrote:

GC> That's because you understand what they mean. To me, they're more like
GC> abstract strings of symbols. Evidently
GC>   s/cmd/[ $(&) ]/
GC> does make a difference in practice, yet I anticipated that $() and [] would
GC> be nilpotent--and, specifically, that I could take a valid conditional like
GC>   if [ "$(id -u)" -eq 0 ]; then ...
GC> and replace everything within "[]" with some command to produce another
GC> valid conditional.

 I think an important thing to understand here is that originally "[]" had
no special meaning whatsoever. In fact, "[" was, and still remains, just a
command with (very) weird name. You can try running "where [" (from zsh) or
"ls -l /usr/bin/\[" to check this.

 Nowadays shells do have special "[[ ... ]]" syntax which, for many good
reasons, should be preferred to "[]", but the latter still works and its
history explains some of its quirks. And, of course, dash doesn't support
"[[", so we're limited to "[" if you want to support any kind of /bin/sh,
including dash.

 BTW, you might know "[" under its alternative name, which is "test". On
my system they're different commands but I'm pretty sure that historically
one of them could be, and was, just a symlink to the other one, although I
don't remember in which sense it went any longer.

GC> > GC> But if I enclose the command in "[ $(...) ]", it "fails":
GC> > 
GC> >  This tests... nothing. $(...) expands to nothing because there is no
GC> > output of curl, as you had taken care to suppress it, and so this is
GC> > equivalent to "[ ]". To be honest, I didn't know what does testing nothing
GC> > does, but it turns out that it always returns false.
GC> 
GC> This:
GC>   if [ ] ; then echo true; else echo false; fi
GC> always prints "false". So why did
GC>   if [ $(id -u) ] ; then echo true; else echo false; fi
GC> always print "true", regardless of `whoami`? The explanation is
GC> that if I'd "taken care to suppress" the output:
GC>   if [ $(id -u 2>&1 >/dev/null) ] ; then echo true; else echo false; fi
GC> then it would print "false" instead. Right?

 Yes. Also, the version without output suppressing prints true, regardless
of your user ID, because "[ N ]" returns true for any number N, even if
this number is 0. However just "[ ]" returns false (which is something I
had never run into until today, so I guess it's not that widely known).

GC> Here's my hypothesis: a shell command is like one of those classic C
GC> functions that returns something and also potentially sets errno:
GC>  - the shell command's return code is like C's errno
GC>  - the C return value is like the shell command's output
GC> Is that a helpful way of looking at it?

 I don't know really know if it's helpful, but it's more or less exact. And
"errno" is called "exit code", or "$?".

GC> >  So this doesn't seem surprising to me at all. But I can't figure out what
GC> > was your intention here, i.e. why do you use the command expansion shell
GC> > construct and what exactly do you expect to obtain from it?
GC> 
GC> Tackling my problematic command
GC>   if [ $(curl https://git.savannah.nongnu.org:443 >/dev/null 2>&1) ]; then
GC> from the inside out, I thought as follows. This is the command I want to 
run:
GC>          curl https://git.savannah.nongnu.org:443
GC> This throws away 27 lines of output that I don't want to see:
GC>          curl https://git.savannah.nongnu.org:443 >/dev/null 2>&1
GC> 
GC> Now adding "$()":
GC>        $(curl https://git.savannah.nongnu.org:443 >/dev/null 2>&1)
GC> means I only want the return code (0 if successful); so far, so good?
GC> No. I already had the return code, without "$()". Adding "$()" means:
GC> throw away the return code, and capture the text output--which would
GC> have been implicitly printed on the screen, except that I discarded it.
GC> Right?

 Yes.

GC> Well, maybe not. Consider the following commands, which
GC>  - test a resolvable URL vs. one spoiled by adding 'X' after "https://";
GC>  - with vs. without "$()"
GC> 
GC> $if curl https://Xgit.savannah.nongnu.org:443 >/dev/null 2>&1; then echo 
true; else echo false; fi 
GC> false
GC> $if curl https://git.savannah.nongnu.org:443 >/dev/null 2>&1; then echo 
true; else echo false; fi 
GC> true
GC> 
GC> $if $(curl https://Xgit.savannah.nongnu.org:443 >/dev/null 2>&1); then echo 
true; else echo false; fi
GC> false
GC> $if $(curl https://git.savannah.nongnu.org:443 >/dev/null 2>&1); then echo 
true; else echo false; fi 
GC> true
GC> 
GC> The effects seem to be the same with or without "$()", so what does the
GC> the "$()" do in this case? And why does it seem to make no difference?

 The difference is that you don't use "[" at all here. What happens with
"if" is that you only test "$?" because this is what "if" does. However
with "[]" you pass it the string containing the test and it doesn't care at
all about the state of "$?" at the moment when this string was generated.
"[", or "test", sets "$?" itself as the result of interpreting its
arguments using its own syntax (see test(1)).


 What can I say, nobody ever pretended shell wasn't confusing. IMHO this is
not the worst part, because typically once you realize the difference
between "if command" and "if test condition", things should be relatively
clear.

 The best advice I can give about shell scripting is to always test
anything you might have a doubt about in a separate snippet to ensure that
it behaves as you expect it to, because failures in the actual scripts are
hard to debug and often lead to unforeseeable weirdness later on.

 Good luck,
VZ

Attachment: pgpMlqRkYzTLV.pgp
Description: PGP signature


reply via email to

[Prev in Thread] Current Thread [Next in Thread]