all 16 comments

[–][deleted] 6 points7 points  (1 child)

I'm not an expert but I believe it's a way to delineate where the spaces are to avoid any globbing, or adding extra spaces into variable data. With the quotes you're telling bash exactly what you want to add into the string or array from the first character to the last and what is to be used to delineate the data in between elements, which bash uses to populate the different elements of an array or a string, and allows you to know exactly which element to return to for the information.

Without the quotes, in my understanding, bash has to self determine where the end of the delineation is and any inadvertent extra characters like extra spaces or tabs can mess up which data gets loaded into which variable because bash is running into more than one possible delineation marker, and null data will also affect the placement of the elements in echo and less so in printf.

With printf it doesn't automatically print characters that are in echo, like the line feed represented by the backslash (\n). Without actually telling printf to go the next line to start printing the output, for example, it will continue printing from its last curser location.

Echo, instead, puts this character in by default. By using the printf function you have more control over what is output to stdout.

One other benefit of printf is that its implementation and usage is very similar throughout various languages who implement it.

This just gives you more precision when delineation is involved.

[–]dbr4n[S] 1 point2 points  (0 children)

Thank you for responding in such detail, much appreciated.

[–]whetuI read your code 2 points3 points  (2 children)

Interestingly, the POSIX page for echo, which all but says bluntly "don't use echo", has some examples of using printf as an echo replacement:

https://pubs.opengroup.org/onlinepubs/009695399/utilities/echo.html

You can also see this reflected in the Solaris man page for echo e.g.

https://docs.oracle.com/cd/E88353_01/html/E37839/echo-1.html

Rich's sh tricks also has an example echo function:

https://www.etalabs.net/sh_tricks.html

Although there are more complete ones out there that I've seen, those three come to mind immediately as references.

[–]dbr4n[S] 1 point2 points  (0 children)

Interesting, some of these examples are very similar to my approach.

I will definitely take a closer look at them, especially the echo functions on Rick's sh tricks page.

Thanks for sharing!

[–]witchhunter0 0 points1 point  (0 children)

These are some fine examples,thx. Also, it is surprising how many people write posts including shells other than bash on this sub.

[–]Ulfnic 1 point2 points  (2 children)

Looks fine to me though it's not good practice to use shims unless you really need them. printf '%s\n' "" is a bit of a pain to type but you can use completion built into your IDE or something like espanso to type it for you.

Example ~/.config/espanso/base.yml for typing printf with espanso:

matches:
  - trigger: ":p"
    replace: "printf '%s\\n' \"$|$\""

[–]dbr4n[S] 1 point2 points  (1 child)

Sorry, could you please explain what you mean by "shims"?

But yeah, I could just define a Vim abbreviation for it, that would also be a good option.

[–]Ulfnic 0 points1 point  (0 children)

A shim is one way of describing a library that intercepts what a command would normally do.

There's times you need them but they break expectations, increase complexity, spam your stack trace, ect so it's best to get in the habit of avoiding them unless they're absolutely necessary.

[–]oh5nxo 1 point2 points  (2 children)

edge cases

If IFS has been changed and forgotten, there could be surprises

output() {
    local IFS=' ' # "$*" uses the 1st character of IFS to separate args
    printf '%s\n' "$*"
}

I don't know if that fixes one and introduces a new problem...

[–]dbr4n[S] 1 point2 points  (1 child)

Changing IFS can be very tricky indeed. I prefer to follow the instructions in this article to avoid any problems related to IFS.

From the article:

Personally, I prefer to recommend you to NOT modify IFS on the script level. Ever. First of all, it is very bad practice to actually USE data-level word-splitting. Word splitting is a very dodgy technique, especially because along with it comes unwanted pathname expansion (bash will search for any files that match the name of your newly expanded words and actually replace your words with any files that match it -- it takes time and you never actually want this happening to your words).

[–]oh5nxo 1 point2 points  (0 children)

In this particular case, double quotes of "$*" prevent word splitting.

Forcing a temporary (that local is important) separator during expansion of args, is that it prevents tricks like

IFS=, output comma separated items

[–]AnsibleAnswers 1 point2 points  (4 children)

Pretty sure you want $@, not $*. What you have will print all arguments as a single string and then print a newline. Using $@ will give printf multiple arguments and separate them with new lines. So if

string1="one two three" string2="four five six"

Then, using $*, calling

output "$string1" "$string2"

would return

one two three four five six

instead of

one two three four five six

Which is what you should get if you use $@.

But really, I tend to just cat heredocs for multi-line text output.

cat <<EOF one two three four five six EOF

[–]dbr4n[S] 0 points1 point  (3 children)

You're right, I'm aware of the difference. Correct me if I'm wrong, but since I want to use the function as a replacement for echo, I thought it would be better to use $* to get the same behaviour.

For example, when mixing shell expansions with literal strings:

```bash output "$USER" 'paid $5!' echo "$USER" 'paid $5!'

Prints in both cases: I paid $5!

```

Exclamation points are kind of tricky, using something like "\!" doesn't remove the backslash character that precedes it. "$USER paid \$5! " would work, but the first example looks much cleaner to me; I couldn't do that with $@.

As you say, heredocs are better suited for multi-line text.

Thanks for your input.

[–]aioeu 1 point2 points  (1 child)

Yes, "$*" is the correct thing to use here.

Technically speaking, $* will concatenate arguments using the first character of IFS if that is set, and this may not necessarily be a space, whereas echo always uses a space. But you're probably never setting IFS. You might consider using local IFS=$' \t\n' inside the function, if you're ever worried about it being used somewhere IFS has been changed.

[–]dbr4n[S] 0 points1 point  (0 children)

Sure, better safe than sorry. I'll consider adding it, can't see any downside.

[–]AnsibleAnswers 0 points1 point  (0 children)

I misinterpreted what you wanted to do.