1

Is there a way to get bash (version 3.2.57(1), if it matters), in the mode where it's emulating sh, to implement the -n option to the echo builtin?

Specifically, I'm curious whether there might be one option I could set somewhere, rather than seeking out and rewriting N calls to echo -n in M different scripts, that had — perhaps only by dumb luck — been working fine for years. Clearly it's possible for bash's echo builtin to implement -n, because in full bash mode, it does.

[Disclaimer: Yes, I know that echo -n is not portable in the first place, and that printf is recommended.]


Side note: Commentators have noted that this version of bash is "17 years old". Someone should tell Apple about this — it's the version that came with MacOS Ventura 13.0 on the reasonably new Mac where I'm typing this.

  • 1
    Use printf instead. – choroba Jan 31 '24 at 14:41
  • @choroba Question updated. – Steve Summit Jan 31 '24 at 14:42
  • 3
    interestingly, typing echo -n foo into sh --posix (bash 5.2) works beautifully. So, yes, might really depend on version (and yours is 17 years old, so things might have reasonably changed!) – Marcus Müller Jan 31 '24 at 14:46
  • I think this is an XY problem. There might be a bunch of ways to do this (use some other echo like in @MarcusMüller's answer, or just turn off POSIX emulation with set +o posix and then run echo (or make echo a wrapper function that does this and then does set -o posix again). But we can't say if any of that is going to be useful for you. – muru Jan 31 '24 at 15:23
  • You can use this in sh: bash -c 'echo -n "Line 1\nAlso Line 1\n"' or you just use /bin/echo. – paladin Jan 31 '24 at 15:23
  • @paladin Using /bin/echo is a decent workaround which I was already considering. I was hoping for an option — and you've given me one! set +o posix is quite likely exactly what I want. Make that an answer, and I'll accept it! – Steve Summit Jan 31 '24 at 15:28
  • @Muru Oh, there's definitely an XY problem lurking here, except that I really am wondering if there's an answer to Y. The answer to "How do I portably echo something without a newline?" is well known. But here I'm really asking, "bash implements echo -n in bash mode. For some reason it makes a different choice in sh mode. Is there a way to tell it to make the same choice, to implement -n in sh mode also? And Stéphane Chazelas has now answered most of that. – Steve Summit Jan 31 '24 at 16:46
  • 1
    @MarcusMüller, I'm thinking it's likely a compile-time option rather than the version. The Bash 3.2 I had compiled with the default options did respect -n even in POSIX mode. There's at least ./configure --enable-xpg-echo-default, but that actually sets xpg_echo, while it's not shown as set in the Mac one. So it might just be that the Mac sh just has some other difference. – ilkkachu Jan 31 '24 at 17:46
  • My own Mac tells me over-and-over that zsh is the new default shell, not bash... I've not switched over... but you might check out the behavior of echo in zsh... – paul garrett Jan 31 '24 at 22:05
  • @paulgarrett, the default interactive shell is zsh. But /bin/sh is still a version of Bash. – ilkkachu Feb 01 '24 at 10:00

6 Answers6

3

The best idea I could come up with is replacing the builtin echo with the "real" echo executable from the GNU coreutils, that is almost certainly installed if you have a bash:

echo() { "$(type -P echo)" "$@" ; }

which simply shadows the built-in.

Since GNU Coreutil's echo.c really isn't very long, and a good part of that is special-case handling for things like "am I running on UNIX System V and am expected to behave like that?", --help printing and similar things you don't strictly need, you might consider just including a small C implementation of echo in your shell scripts and put it through the POSIX-specified c99 if you know that you got that. Heck, this is a machine from the 2000s, so you might as well assume you have perl and implement the same in perl.

  • "this is a machine from the 2000s" Or it might be macOS – muru Jan 31 '24 at 15:09
  • @muru Indeed. – Steve Summit Jan 31 '24 at 15:12
  • heh, interesting! – Marcus Müller Jan 31 '24 at 15:18
  • Or create a shell script to implement echo :) Somehow, running type in a subshell on every invocation of that function feels bad to me. I'd be tempted to do something like eval "echo() { '$(type -P echo)' \"\$@\" ; }" instead (taking the risk of single quotes in the path). Or just store the path in a variable. It's not likely it's likely to change anyway... – ilkkachu Jan 31 '24 at 17:12
3

As seen at Why is printf better than echo?, bash doesn't support the -n option (nor -e/-E) when both the posix option is enabled (like when invoked as sh) and the xpg_echo option is enabled (as it is by default on systems where bash is built so as to be UNIX compliant).

So to have bash's echo support -n (and -e/-E and combinations thereof like echo -en, echo -e -n...), you need to disable either or both of those options. If you want that sh to still be UNIX-compliant except for that -n, it's probably xpg_echo only that you want to turn off:

shopt -u xpg_echo

Example:

$ bash -o posix -O xpg_echo -c 'echo -n a'
-n a
$ bash -o posix -O xpg_echo -c 'shopt -u xpg_echo; echo -n a'
a

bash is still mostly UNIX-compliant, except for the -n that was not printed.

$ bash -o posix -O xpg_echo -c 'set +o posix; echo -n a'
a

That -n handling is enabled, but also some other non-UNIX compliances so it behaves even less like standard sh.

  • Yowza. I was hoping for an option like xpg_echo — that looks like precisely what I want. Thank you very much. Unfortunately, although it exists in this version of bash, it doesn't seem to behave as described. I'll play with it some more tonight. Thanks again. – Steve Summit Jan 31 '24 at 16:06
  • On my Mac, it shows xpg_echo is off for sh. I suppose the behaviour might be set by some hard compile-time setting? – ilkkachu Jan 31 '24 at 17:09
  • @ilkkachu, it works for me as I describe with a bash 3.2.57 built with configure --enable-xpg-echo-default --enable-strict-posix-default. Presumably Apple has broken it further so it behave like the xpg_echo way even when xpg_echo is off. In any case, that would be a bug as it would not work as documented (where xpg_echo off means -e/-E/-n are handled). – Stéphane Chazelas Feb 01 '24 at 09:01
1

You can define a function with the necessary functionality

echo() ( IFS=' ' nl='\n'; [ "$1" = '-n' ] && shift && nl=; printf "%s$nl" "$*" )

In dash (admittedly not bash in sh mode, and definitely not the version you have), this avoids external references.

Note that POSIX says the only guaranteed compatible way to use echo is without arguments or backslash escapes:

If the first operand is -n, or if any of the operands contain a <backslash> character, the results are implementation-defined.

If you want the extended version that handles backslash escapes, use %b instead of %s:

echo() ( IFS=' ' nl='\n'; [ "$1" = '-n' ] && shift && nl=; printf "%b$nl" "$*" )
Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • true! that works and is probably faster; I don't know posix enough to know whether we have to care about things like echo -e -n '\t\tfoo' – Marcus Müller Jan 31 '24 at 15:39
  • "$*" joins the positional parameters with the first character of $IFS, so you'd also need a local IFS=' '. Also, standard echo is meant to escape \x sequences, so %b may be preferable (maybe not if the OP expects echo to work in a non-standard fashion). – Stéphane Chazelas Jan 31 '24 at 15:59
  • @StéphaneChazelas given that the only POSIX guaranteed echo is one that does not handle either -n or escaped characters I'm not overly worried. The IFS issue is a fair point though and I'll address that now – Chris Davies Jan 31 '24 at 16:44
  • @ChrisDavies, UNIX (POSIX with XSI option) guarantees that no option are supported and that escape sequences are expanded. Without XSI, you get no guarantee at all. – Stéphane Chazelas Jan 31 '24 at 18:52
1

The external /bin/echo on my Mac does support -n, and you can disable the Bash builtin one with enable -n:

bash-3.2$ enable -n echo
bash-3.2$ type echo
echo is /bin/echo
bash-3.2$ echo -n foo |od -c
0000000    f   o   o
0000003
ilkkachu
  • 138,973
1

Fix the Underlying Issue

Google will return a plethora of results if you use "Upgrade bash macOS" as the search term. Using a native exchange answer, and combining 2 answers into one (Per your Side note):

Frequently Asked Questions about the GNU Licenses: Tivoization

When people distribute User Products that include software under GPLv3, section 6 requires that they provide you with information necessary to modify that software. User Products is a term specially defined in the license; examples of User Products include portable music players, digital video recorders, and home security systems.

Since Apple's Darwin OS has been closed source since about 10.5, providing an upgraded BASH would violate the above paragraph, so Apple does know. There is a way to update the BASH shell and even make it the default:

Install Updated Bash Via Homebrew

Paste the following in your terminal:

  1. /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" as one line.
  2. Issue brew install bash

This should install an updated version of BASH in Homebrew which is then symlinked. Use which bash to find the location To set this as the default:

  1. Edit /etc/shells.
  2. Add the path + the binary to the file at the end.
  3. Logout and back in to update your environment.
  4. Use the chsh command to "Change Shells", providing the path to the new BASH version. This should make the new version your default shell.

Sources

eyoung100
  • 6,252
  • 23
  • 53
  • The question here though is about the echo builtin of /bin/sh on macos which is a build of an ancient version of bash (for the reason you mention) configured with --enable-strict-posix-default --enable-xpg-echo-default – Stéphane Chazelas Jan 31 '24 at 20:04
  • @StéphaneChazelas I totally agree that the question is RE: echo, and I put this answer here as an option to the OP if he chooses to do so. Personally I would but it's not up to me. Using this answer would possibly alleviate the need for a fix in the first place, but if I've learned anything from contributing here, it's that there are about a million different solutions to every problem. – eyoung100 Jan 31 '24 at 22:08
-2

On the macOS 14.3 version of bash, unsetting posix seems to be enough:

% /bin/sh -c 'set +o posix; echo -n foo; set -o posix; echo -n bar'
foo-n bar
% /bin/sh --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)
Copyright (C) 2007 Free Software Foundation, Inc.
muru
  • 72,889