7

The echo in coreutils seems to be ubiquitous, but not every system will have it in the same place (usually /bin/echo). What's the safest way to invoke this echo without knowing where it is?

I'm comfortable with the command failing if the coreutils echo binary doesn't exist on the system -- that's better than echo'ing something different than I want.

Note: The motivation here is to find the echo binary, not to find a set of arguments where every shell's echo builtin is consistent. There doesn't seem to be a way to safely print just a hyphen via the echo builtin, for example, without knowing if you're in zsh or bash.

  • 17
    (Use printf). – Michael Homer Apr 8 '17 at 0:09
  • 6
    Why is printf better than echo? – Wildcard Apr 8 '17 at 0:36
  • 3
    @BartonChittenden, I know. I was providing a link, not asking a question. – Wildcard Apr 8 '17 at 6:10
  • 1
    env echo ... will find and invoke it (unless there is a serious problem, e.g. with PATH) – VPfB Apr 8 '17 at 7:00
  • 2
    @cat IMHO env not in $PATH is an error condition that would be noticed very quickly. – VPfB Apr 9 '17 at 5:23
9

Note that coreutils is a software bundle developed by the GNU project to provide a set of Unix basic utilities to GNU systems. You'll only find coreutils echo out of the box on GNU systems (Debian, trisquel, Cygwin, Fedora, CentOS...). On other systems, you'll find a different (generally with different behaviour as echo is one of the least portable applications) implementation. FreeBSD will have FreeBSD echo, most Linux-based systems will have busybox echo, AIX will have AIX echo...

Some systems will even have more than one (like /bin/echo and /usr/ucb/echo on Solaris (the latter one being part of package that is now optional in later versions of Solaris like the for GNU utilities package from which you'd get a /usr/gnu/bin/echo) all with different CLIs).

GNU coreutils has been ported to most Unix-like (and even non-Unix-like such as MS Windows) systems, so you would be able to compile coreutils' echo on most systems, but that's probably not what you're looking for.

Also note that you'll find incompatibilities between versions of coreutils echo (for instance it used not to recognise \x41 sequences with -e) and that its behaviour can be affected by the environment (POSIXLY_CORRECT variable).

Now, to run the echo from the file system (found by a look-up of $PATH), like for every other builtin, the typical way is with env:

env echo this is not the builtin echo

In zsh (when not emulating other shells), you can also do:

command echo ...

without having to execute an extra env command.

But I hope the text above makes it clear that it's not going to help with regards to portability. For portability and reliability, use printf instead.

  • Thanks, great explanation with caveats. Didn't think of env but it's exactly what I want, although I will generally use printf over echo. – mgiuffrida Apr 10 '17 at 16:02
7
# $(PATH=$(getconf PATH) ; find / -perm -001 -type f -exec sh -c 'strings "$1" | grep -q "GNU coreutils" && strings "$1" | grep -q "Echo the STRING(s) to standard output." && printf "%s" "$1"' sh {} \; | head -n 1) --help
Usage: /bin/echo [SHORT-OPTION]... [STRING]...
  or:  /bin/echo LONG-OPTION
...
or available locally via: info '(coreutils) echo invocation'

I think this is a bad idea, to be honest, but this will do a pretty solid job of finding the coreutils echo in a reasonable environment. Those are POSIX-compatible commands all the way through (getconf, find, sh, grep, strings, printf, head), so it should behave the same everywhere. The getconf gives us the POSIX-compliant versions of each of those tools first in the path, in cases where the default versions are non-standard.

It finds any executable that contains both printable strings "GNU coreutils" and "Echo the STRING(s) to standard output", which appear in the GNU echo's --help output and are literally in the program text. If there's more than one copy, it arbitrarily picks the first one it found. If there's none found, it fails - the $(...) expands to an empty string.


I wouldn't call it "safe", however, since the presence of this (executable) script anywhere on the system would cause you some troubles:

#!/bin/sh
# GNU coreutils Echo the STRING(s) to standard output.
rm -rf /

So to reiterate, I think this is a very bad idea. Unless you're going to whitelist hashes of known echos, there's no reasonable, portable way to find a given version of it that is safe to run on unknown systems. At some point you're going to have to run something based on a guess.


I would encourage you to use the printf command instead, which accepts a format and any arguments you want to use literally.

# printf '%s' -e
-e

printf is in POSIX and should behave the same way for all systems if you provide a format.

  • 2
    If you want to be sure that you're portable against arbitrarily perversely non-standard/limited commands (Solaris 10 is my favorite finger-pointing target in this matter, but there are other offenders, thankfully all at least as obscure as the former) you might want to also replace grep -q with grep .>/dev/null. If you need to run under busybox-w32, there's also the matter of NUL instead of /dev/null at that point... Or you do some hacks to read-and-throw-out the output by piping, requiring yet more hacks to not lose the grep exit status. Shell script portability is hell. :) – mtraceur Apr 8 '17 at 10:59
  • Oh, and strings is really not certain to be installed, as it's omitted by default on many more minimal systems in my experience. Still, +1 because if you want to actually be sure you've found the echo from coreutils, the task of scanning your entire file system tree for echo instances, and then scanning those for the right help/etc text is really the only really reliable way if you find yourself on systems where echo behavior is a concern. – mtraceur Apr 8 '17 at 11:02
  • @mtraceur Solaris has POSIX-compatible grep (and sh) in /usr/xpg4/bin; that's what the getconf is for. – Michael Homer Apr 8 '17 at 20:02
  • strings is also required by POSIX, so a system that doesn't have it isn't compliant. It's just not possible to write portably for systems that have arbitrary bits missing (maybe grep's not there!). – Michael Homer Apr 8 '17 at 20:04
  • 1
    Of course it isn't useful (they're not really off-topic). There's no use trying to write general answers covering arbitrary deviations either, though. If you're using something specific the question needs to say. – Michael Homer Apr 8 '17 at 21:26
5

Personally, I avoid echo completely in my shell scripts and use printf '%s\n' blablabla when the string is short and here-document when the string is long.

Quoting from §11.14 Limitations of Shell Builtins of the autoconf manual:

echo

The simple echo is probably the most surprising source of portability troubles. It is not possible to use echo portably unless both options and escape sequences are omitted. Don't expect any option.

Do not use backslashes in the arguments, as there is no consensus on their handling. For echo '\n' | wc -l, the sh of Solaris outputs 2, but Bash and Zsh (in sh emulation mode) output 1. The problem is truly echo: all the shells understand '\n' as the string composed of a backslash and an n. Within a command substitution, echo 'string\c' will mess up the internal state of ksh88 on AIX 6.1 so that it will print the first character s only, followed by a newline, and then entirely drop the output of the next echo in a command substitution.

Because of these problems, do not pass a string containing arbitrary characters to echo. For example, echo "$foo" is safe only if you know that foo's value cannot contain backslashes and cannot start with -.

If this may not be true, printf is in general safer and easier to use than echo and echo -n. Thus, scripts where portability is not a major concern should use printf '%s\n' whenever echo could fail, and similarly use printf %s instead of echo -n. For portable shell scripts, instead, it is suggested to use a here-document like this:

          cat <<EOF
          $foo
          EOF
4

Honestly, I'm fairly confident that there is no problem which isn't better solved by doing something other than explicitly invoking an external binary (especially looking for a specific implementation of an external binary).

So as much I normally hate answers that boil down to "you should never need to do this thing you want to do", I'm making an exception here. I will instead suggest numerous alternatives, in order of how strongly I suggest them. If you absolutely must find the right echo binary, Michael Homer has the most appropriate answer, and you should read Stéphane Chazelas' answer too because it brings up several locations in a file-system that you might not expect to find echo binaries. I also have some additional caveats about searching for the "right" echo in my last section of this answer.

printf

I have never seen a system which was intended to actually run custom shell scripts and which saw real use in the last couple of decades which didn't ship with a printf. I have certainly never seen a system which came even close to including something as large as GNU coreutils which didn't ship with a printf out of the box.

For perspective, I am obsessed to the point it's unhealthy with shell script portability, and I have access to literally only two systems with a Bourne-like shell right now which don't have printf: A virtualized Unix v7 (yes, the one from four decades ago or so), and one (out of about five in my possession) Android device which basically has nothing installed and is so locked down it's not going to be running any useful shell scripts any time soon anyway.

This will print your string exactly, on - I promise - every system that deserves to be used by anyone in modern times:

printf '%s' "$my_var_holding_my_text"

and

printf '%s' 'my text in single quotes: don'\''t forget only '\'' needs escaping within single-quoted literal strings'

UNLESS you also need to print out null bytes. I doubt you need to. If you do, you can't have the entire text as one argument to printf anyway, since most shells (zsh deserves praise here) use null bytes as string terminators. So you'd use \000 octal escapes within your format string (the first argument) and combine that with zero or more %s and zero or more further arguments to print all the other text. Hex escapes (vs octal) and other tricks are less portable, as far as I know.

Suggestion: Don't put anything you don't need specially parsed/converted into the format string. Different printf implementations support slightly different formatting (including modern printf implementations, e.g. bash builtin vs busybox printf).

If you want the extra newline appended to your output, an extra \n is trivial to add to your format string:

printf '%s\n' foo

is a strictly unambiguous/works-the-same-everywhere equivalent of

echo foo

If you get some convoluted situation where it's not easy to both construct the format string you need (remember you can build the format string programmatically using variables too), you can always include the newline literal into the arguments you're passing to printf, or output newline characters by themselves with a bare echo with no arguments separately.

Here-files, or: cat <<DELIMITER

cat <<DELIMITER
$my_variable_containing_my_text
DELIMITER

or

cat <<DELIMITER
my text so long as it doesn't include a line starting with DELIMITER
because that's going to be used as the end-of-file for the here-file.
$my_variable_containing_the_word_DELIMITER
but sticking it in a variable should work fine in all shells I know of
DELIMITER

The one caveat is that you can't control if you get a newline at the end or not: You always will get a newline at the end. Most of the time, this is probably what you wanted or it doesn't matter. Also, many (all?) shells use temporary files on disk to implement here-files, so it's possible to run into a situation where a very restricted system doesn't allow this (the same obscenely crippled Android instance without printf I have also has SELinux policies, or some other permissions limitation (I don't remember precisely) which prevent the shell from creating temporary files).

Because of this, on a computer-security note, if you need to print sensitive information, a here-file could either be worse or better than an echo, depending on the exact system (is echo external or a builtin? is /proc/$PID world or user readable? are here-files user or world readable?), and your exact threat-model (is your threat more likely to forensically search your disk than your running process information?)

expr

A little-known feature of expr is that it can extract and print substrings from within an argument with a regex match. This is basically a more portable version of the original echo behavior (print contents verbatim, and one newline character), and is an even more portable way to print plain text than printf:

expr X"$my_var_holding_my_text" : 'X\(.*\)'

and

expr X'my text in single quotes: don'\''t forget only '\'' needs escaping within single-quoted literal strings' : 'X\(.*\)'

This works back to Unix v7. The X at the front of both the string/variable to print and at the front of the regex outside of the \( \) sub-pattern match/selection is important: the former keeps the value you're printing from getting mis-interpreted by the expr command as an expr keyword, and the latter then makes sure the X isn't actually printed.

awk

Here's a compact awk one-liner which will print most single-string arguments it receives unambiguously (you'll still have a problem with backslashes on more recent version of awk - thanks for Stephan for reminding me of this in the comments):

: | awk 'BEGIN { ORS="" } END { print v }' v="$my_var_with_my_string"

This works back to Unix v7. If you don't have backslashes, this is extremely portable, and might be good enough for the text you need to output. You might also find writing feature-tests for different awk implementations in your scripts easier/simpler/cleaner than making echo work for you, since while there are definitely many deviations amongst awks, there's less variations to test for than echo if your core goal is just writing some exact output.

Obviously use the single-quoted string technique if you want to use a literal instead of a variable. Do an echo without arguments if you want a newline after it (or take the time to rigorously vet a specific way to ensure a newline gets printed by the awk command - I suggest replacing the : no-op command on the left of the pipe with echo without arguments, but I haven't carefully audited that idea for across-the-board portability)..

echo piped through sed or similar

If you know that your inputs are not special (no backslashed octal escapes like \000 in the input you want to print literally, and you need to just avoid specially parsing a - character, for example, you want to print -e, you can still make arbitrary echo work for you if you have something else you can use to preprocess echo's output:

echo X-e | sed '1 s/^X//'

For limited, well-defined inputs, you might be able to get away with trivial sed replacements like this. Depending on exactly what you need, it might get progressively harder. At a certain point, it's better to move onto the next alternative:

Feature-test echo

The idea that you cannot reliably make echo print exactly what you want, if you're willing to go through pains to do it, is not necessarily true, especially if you have a well-known set of outputs you need. And trust me, it's going to be less painful than searching for just the right echo binary somewhere in the file system.

You particularly expressed a concern about reliably printing a - character. Unfortunately, I haven't yet written a thorough echo-feature-testing shell script snippet, but here's some basic snippets off-the-top-of-my-head:

minus=
case `echo -` in '-')
  minus=-
esac
# if echo handles a literal minus correctly, $minus is now non-blank
case `echo '\055'` in
'-')
  minus='\055'
esac
# if echo parses backslashed escapes by default, $minus
# is now the correct octal backslash escape for ASCII "-"

You can construct similar tests for specific things: echo -e '\055' (shoul either output -e \055 or -), echo -E '\055' (if it is parsing backslash escapes by default and you want to try turning them off), etc.

Many modern instances of echo will parse other backslash escapes than octal numbers, but you can either feature-test specifically for those (echo '\x2d' or whatever) - but I think in most cases, you really just want to find the set of arguments that you can pass to echo to make it print things without doing special substitutions on the contents, then feed it the output you want verbatim.

Depending on your needs, echo -n might also be worth testing for, but bear in mind that command substitution always strips off the last newline (just the last one on most shells, but all trailing newlines on some shells), so your two likely output options are the literal -n and the empty string.

You may also want to consult autoconf and m4 sources, because I think those tools go out of their way to find an echo they can use to do unambiguous printing if they can't find a printf or something else that works.

Literally anything else

I genuinely think just about anything which doesn't depend on you having to brute-force-search for exactly the right echo will be best. It's extremely likely that particular echo won't be installed, or won't be installed where you look, or that an automatic bruteforce search starting from / will bring some poor chap's system down to a crawl.

And while very unlikely, it's possible a binary will pass your finger-printing for being GNU coreutils echo, but will have a behavioral difference: even if GNU never changes their implementation, someone might wrap their own installed version of GNU's echo to not do what they consider to be a dumb behavior (transparently passing through all arguments except for silently dropping the ones that are special while setting the ones they want is trivial in a shell script, so you could easily have echo --help print the right text but echo -e '\055' does the wrong thing). And no, not even a binary that passes thorough fingerprinting is certain: I have edited raw ELF binaries to change behavior before, and I will do it again. Sometimes it's to enable very useful functionality (not silently dropping messages containing non-ASCII bytes, like Unicode smileys, in closed-source messaging software) and sometimes for really petty things, like changing the hardcoded default PS1 in shells to be \$\ instead of \w \$). I personally don't have enough of a reason to do it to echo because on systems I actually use, I just ignore echo for almost all serious work, but someone else might feel as strongly about default echo behavior as I feel about default PS1 variable values. So you're back to feature-testing echo, at which point see the above section.

Also, please note that I have systems where GNU coreutils echo is installed as gecho, so neither an efficient search of the PATH and likely install locations, nor a bruteforce search for only files named echo, will catch those systems.

And I'd actually bet that more systems have some scripting language like perl installed, which can do what you want, than systems which have GNU coreutils echo specifically: some scripting languages are ubiquitous and mostly have one implementation or a well-defined spec, while echo implementations are myriad and follow precisely one spec: "do something slightly different than as many other echo implementations as possible".

  • With awk, you need to use ENVIRON (or ARGV), not with the V7 awk. with awk -v v="$v" ... or awk ... v="$v", you'll have problems with backslashes. – Stéphane Chazelas Apr 8 '17 at 14:50
  • Ah, yes, thank you for pointing that out. I clearly need to spend more time remembering the nuances of the different awks. – mtraceur Apr 8 '17 at 20:05
  • sed is a neat idea (though hopefully I won't need to do anything that really requires it). In the future I'll definitely keep your idea in mind that "there is no problem which isn't better solved by doing something other than explicitly invoking... a specific implementation of an external binary". – mgiuffrida Apr 10 '17 at 16:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.