5

In a script, I've found a curious way to process systemctl status's output:

echo "$(systemctl status the_unit_name)" | grep -q 'Active: active'

instead of the obvious:

systemctl status the_unit_name | grep -q 'Active: active'

I couldn't find any valid reason for using that approach. Is there a reason I am missing?

ilkkachu
  • 138,973
Marcus
  • 961
  • I don't have a system available to test, so I'll just leave this as a comment, but it might have something to so with systemctl paginating its output by default (at least older versions did this). – Jeremy Dover Jan 04 '24 at 13:45
  • 1
    @JeremyDover, I wonder if there'd be a difference with that here, both versions above should have systemctl's stdout connected to a pipe (or a socket), which should disable any auto-pagination if it works sensibly. Most of the time what matters is if the stdout is connected to a terminal. – ilkkachu Jan 06 '24 at 09:54
  • @ilkkachu Your comment is likely correct on modern versions, but I seem to remember (though it has been a long time) that the initial implementations of systemctl's pagination did not behave sensibly, and in particular remember having trouble grepping through its output. I do agree I'm not sure why the syntax given would fix the problem, except that it does not hook systemctl's STDOUT to a pipe; rather bash executes the command in a subshell and stores the result in a variable. – Jeremy Dover Jan 06 '24 at 12:37
  • @JeremyDover, yes... but how do you imagine Bash gets to read the output to be able to store it in a variable? In both the pipe and the command substitution, the output goes to another process, one way or another. Setting up a pipe is obvious way to do that (in command substitution, it's just a pipe to the Bash interpreter itself), but a socket could work similarly, and in the case of the command substitution, the shell could use a temporary file too (I guess). So there might be a difference in what kind of an fd systemctl sees, but I doubt there is one. – ilkkachu Jan 06 '24 at 15:40
  • Anyway, we could try with something like ispipe() { perl -e 'printf STDERR "STDOUT is a pipe: %s\n", (-p STDOUT ? "yes" : "no")'; }; I get a positive answer for both ispipe |cat and a=$(ispipe) in Bash. (But in the ksh I have, the pipe is done using a socket, and the command substitution with a file...) – ilkkachu Jan 06 '24 at 15:46

2 Answers2

10

There’s no reason, other than possibly ignoring systemctl’s exit status (that would depend on the surrounding setup). There might be historical factors explaining why the command is written that way, but there’s no reason to keep it that way.

See also The "proper" way to test if a service is running in a script.

Stephen Kitt
  • 434,908
  • I think the reason is ignoring systemctl's exit status indeed, and that the existing code is just a confusing way of expressing this intent. Thanks! – Marcus Jan 06 '24 at 09:14
6

In general, echo "$(foo)" is silly and pretty much equivalent to just foo. The difference there is that the command substitution removes all trailing newlines from output of foo, and echo adds exactly one (and might process backslashes, but let's assume it doesn't). Usually, that doesn't change anything, since the output is likely to have exactly one in the end.

But if the output of foo is missing the final newline, it does mean that the combination of echo and command substitution fixes that, making sure that the right-hand side of the pipe gets a proper text file as input. That's important in principle, since technically the POSIX standard requires that the inputs must be proper text files, apparently leaving open the possibility of unexpected behaviour if they aren't.

In practice, I've never seen an implementations of grep that croaked on such a technically invalid input, or treated the final newline-less line fragment any differently than other lines. Considering how grep works, essentially ignoring the newlines, it's not obvious how misbehavior on that could easily happen. (Also, fgets(), the C standard function for reading a line also doesn't really care if there is a newline at the end or not, unlike e.g. the shell's read, where the exit status depends on if a newline was seen or not. Then again, if the utility doesn't check what it read, assumes there must be a newline at the end, and removes the final character unconditionally, with something like buf[strlen(buf) - 1] = '\0', then that would eat the final regular character if the final newline was missing.)

That said, I also don't believe that's actually the reason they went for that odd-looking command. (And I don't think systemctl produces improper output.) It's morely likely that they just weren't really thinking what they were doing.

ilkkachu
  • 138,973
  • +1 for " It's morely likely that they just weren't really thinking what they were doing." that along the line of ps -ef | grep fixed-string | grep -v grep – Archemar Jan 03 '24 at 19:20
  • I've had to do that far too many times, why would you call that "not really thinking"? – This isn't my real name Jan 03 '24 at 19:45
  • @Thisisn'tmyrealname, had to do echo "$(foo)"? Why? The "they probably weren't thinking" comes from too much exposure to questions on SE, I've seen that unnecessary construct in a few questions here. – ilkkachu Jan 03 '24 at 20:09
  • No, I've had to do ps -ef | grep fixed-string | grep -v grep, because the grep fixed-string found itself in the output of the ps -ef, giving me spurious data in its own output – This isn't my real name Jan 03 '24 at 20:13
  • @Thisisn'tmyrealname, right, right. I think they implied that something like | grep '[f]ixed-string' would be better (it matches the same string, but doesn't itself contain it in the same form, fixing the issue). But IMO, just tacking on the grep -v grep is obvious enough, simple, and mostly just works. At least until the process name you're looking for is something like foogrep. – ilkkachu Jan 03 '24 at 20:19
  • It seems that the reason was to ignore systemctl's exit code (although done in a confusing way). – Marcus Jan 06 '24 at 09:14
  • @Marcus, mmh, maybe, I guess. But by default only the exit status of the rightmost part of a pipeline matters, you'd have to explicitly set -o pipefail (or peek into ${PIPE_STATUS[@]}) to see the exit status of the left side. Then again, if the script was written by those silly people who have a penchant for writing set -euo pipefail at the start of each script, then yes, they might feel a need to resort to silly workarounds like that. (I'd go with systemctl ... || true if the intent is to ignore the exit status, though that looks even worse within the pipeline.) – ilkkachu Jan 06 '24 at 09:52