2024-10-21
Quoting and not quoting command substitution in the Bourne shell
Over on the Fediverse, I said something:
Bourne shell trivia of the day:
var=$(program ...)
is the same as
var="$(program ...)"
so the quotes are unnecessary.But:
program2 $(program ...)
is not the same as:
program2 "$(program ..)"
and often the quotes are vital.(I have been writing the variable assignment as var="$(...)" for ages without realizing that the quotes were unnecessary.)
This came about because I ran an old shell script through shellcheck, which recommended replacing its use
of var=`...`
with var=$(...)
, and then I got to wondering why
shellcheck wasn't telling me to write the second as var="$(...)"
for safety against multi-word expansions. The answer is of course
that multi-word expansion doesn't happen in this context; even if
the $(...) produces what would normally be multiple words of output,
they're all assigned to 'var
' as a single word.
On the one hand, this is what you want; there's almost no circumstance
where you want a command that produces multiple words of output to
have the first word assigned to 'var
' and then the rest interpreted
as a command and its arguments.
On the other hand, the Bourne shell is generally not known for being
friendly about its quoting. It would be perfectly in character for
the Bourne shell to require you to quote the '$(...)
' even in
variable assignment.
On the one hand, shellcheck doesn't complain about the quoted version
and it's consistent with quoting $(...)
in other circumstances
(when it really does matter). On the other hand, you can easily
forget or not know (as I did) that the quoting is unnecessary here,
and then you can be alarmed when you see an unquoted 'var=$(...)
'
in the wild or have it suggested. Since I've mostly written the
quoted version, I'll probably continue doing so in my scripts unless
I'm dealing with a script that already has some unquoted examples,
where I should probably make everything unquoted so that no one
reading the script in the future ever thinks there's a difference
between the two.
2024-10-18
The Go module proxy and forcing Go to actually update module versions
Suppose, not hypothetically, that you have two modules, such as a
program and a general
module that it uses. Through
working on the program, you realize that there are some bugs in the
general module, so you fix them and then test them in the program
by temporarily using a replace
directive, or perhaps a workspace. Eventually you're satisfied
with the changes to your module, so you commit them and push the
change to the public repository. Now you want to update your program's
go.mod
to use the module version you've just pushed.
As lots of instructions will tell you, this is straightforward; you
want some version of 'go get -u
', perhaps 'go get -u .
'. However,
if you try this immediately, you may discover that Go is not updating
the module's version. No matter what you do, not even removing the
module from 'go.mod
' and then go-get'ing it again, will make Go
budge. As far as Go seems to be concerned, your module has not
updated and the only available version is the previous one.
(It's possible that 'go get -u <module>@latest
' will work here,
I didn't think to try it when this happened to me.)
As far as I can tell, what is going on here is the Go module proxy. By default, 'go get
' will consult
the (public) Go module proxy, and the Go module proxy can have a
delay between when you push an update to the public repositories
and when the module proxy sees it. I assume that under the hood
there's various sorts of rate limiting and other caching, since I
expect neither the Go proxy nor the various forges out there want
the Go proxy to query forges on every single request just in case
an infrequently updated module has been updated this time around.
The blunt hammer way of defeating this is to force 'go get -u
'
to not use the Go module proxy, with 'GOPROXY=direct go get -u
'.
This will force Go to directly query the public source and so make
it notice your just-pushed update.
PS: If you tagged a new version I believe you can hand edit your
go.mod
to have the new version. This is more difficult if your
module is not officially released, has no version tags, and is using
the 'v0.0.0-<git information>' format in go.mod
.
PPS: Possibly there is another way you're supposed to do this. If so, it doesn't seem to be well documented.
2024-10-02
Go's new small language features from 1.22 and 1.23 are nice
Recently I was writing some Go code involving goroutines. After I was done, I realized that I had
used some new small language features added in Go 1.21 and Go 1.22, without really thinking about
it, despite not having paid much attention when the features were
added. Specifically, what I used are the new builtins of max()
and min()
, and 'range over integers' (and also a use of clear()
,
but only in passing).
Ranging over integers may have sounded a bit silly to me when I first read about it, but it turns out that there is one situation where it's a natural idiom, and that's spawning a certain number of goroutines:
for range min(maxpar, len(args)) { wg.Add(1) go func() { resolver() wg.Done() }() }
Before Go 1.21, I would have wound up writing this as:
for i := 0; i < maxpar; i++ { [...] }
I wouldn't have bothered writing and using the function equivalent of
min()
, because it wouldn't be worth the extra hassle for my small
scale usage, so I'd always have started maxpar
goroutines even if some
of them would wind up doing nothing.
The new max()
and min()
builtins aren't anything earthshaking,
and you could do them as generic functions, but they're a nice
little ergonomic improvement in Go. Ranging over integers is something
you could always do but it's more compact now and it's nice to
directly see what the loop is doing (and also that I'm not actually
using the index variable for anything in the loop).
(The clear()
builtin is nice, but it also has a good reason for
existing. I was only using it on a slice,
though, where you can fully duplicate its effects.)
Go doesn't strictly need max()
, min()
, and range over integers
(although the latter is obviously connected to ranging over functions,
which is important for putting user container types closer to par
with builtin ones). But adding them
makes it nicer, and they're small (although growing the language
and its builtins does have a quiet cost), and Go has never presented
itself as a mathematically minimal language.
(Go will have to draw the line somewhere, because there are a lot of little conveniences that could be added to the language. But the Go team is generally conservative and they're broadly in a position to not do things, so I expect it to be okay.)