Elegant way to prevent command substitution from removing trailing newline

I’m customizing my zsh PROMPT and calling a function that may or may not echo a string based on the state of an environment variable:

function my_info {
    [[ -n "$ENV_VAR"]] && echo "Some useful informationn"
}

local my_info='$(my_info)'

PROMPT="${my_info}My awesome prompt $>"

I would like the info to end on a trailing newline, so that if it is set, it appears on its own line:

Some useful information
My awesome prompt $>

However, if it’s not set, I want the prompt to be on a single line, avoiding an empty line caused by an unconditional newline in my prompt:

PROMPT="${my_info}  # <= Don't want that :)
My awesome prompt $>"

Currently I work around the $(command substitution) removing my newline by suffixing it with a non-printing character, so the newline isn’t trailing anymore:

[[ -n "$ENV_VAR"]] && echo "Some useful informationnr"

This is obviously a hack. Is there a clean way to return a string that ends on a newline?

Edit: I understand what causes the loss of the trailing newline and why that happens, but in this question I would specifically like to know how to prevent that behaviour (and I don’t think this workaround applies in my case, since I’m looking for a “conditional” newline).

Edit: I stand corrected: the referenced workaround might actually be a rather nice solution (since prefixing strings in comparisons is a common and somewhat similar pattern), except I can’t get it to work properly:

echo "Some useful informationn"x
  [...]
PROMPT="${my_info%x}My awesome prompt $>"

does not strip the trailing x for me.

Edit: Adjusting the proposed workaround for the weirdness that is prompt expansion, this worked for me:

function my_info {
    [[ -n "$ENV_VAR"]] && echo "Some useful informationn"x
}

local my_info='${$(my_info)%x}'

PROMPT="$my_info My awesome prompt $>"

You be the judge if this is a better solution than the original one. It’s a tad more explicit, I think, but it also feels a bit less readable.

Asked By: dtk

||

You can have your function output the escaped escape codes, so that when it is expanded it turns into the newline.

$ function my_info {
>    [[ -n "$ENV_VAR"]] && echo 'Some useful information\n'
>}
$ echo $(my_info)
Some useful information

$
Answered By: spelufo

Final newlines are removed from command substitutions. Even zsh doesn’t provide an option to avoid this. So if you want to preserve final newlines, you need to arrange for them not to be final newlines.

The easiest way to do this is to print an extra character (other than a newline) after the data that you want to obtain exactly, and remove that final extra character from the result of the command substitution. You can optionally put a newline after that extra character, it’ll be removed anyway.

In zsh, you can combine the command substitution with the string manipulation to remove the extra character.

my_info='${$(my_info; echo .)%.}'
PROMPT="${my_info}My awesome prompt $>"

In your scenario, take care that my_info is not the output of the command, it’s a shell snippet to get the output, which will be evaluated when the prompt is expanded. PROMPT=${my_info%x}… didn’t work because that tries to remove a final x from the value of the my_info variable, but it ends with ).

In other shells, this needs to be done in two steps:

output=$(my_info; echo .)
output=${output%.}

In bash, you wouldn’t be able to call my_info directly from PS1; instead you’d need to call it from PROMPT_COMMAND.

PROMPT_COMMAND='my_info=$(my_info; echo .)'
PS1='${my_info%.}…'