1
printf "%*s\n" $(((${#fname}+$COLUMNS)/2)) "$fname"

I get this error:

line 9: (7+)/2: syntax error: operand expected (error token is ")/2")

This works in the terminal but not in my script. Do you have any ideas?

AdminBee
  • 22,803

3 Answers3

5

Not all shells set the $COLUMNS variable to the width of the terminal.

bash versions prior to 5.0 only set it when interactive, not in scripts. However, since version 4.3, you can still enable it in non-interactive shells with shopt -s checkwinsize.

In either case, there is however a twist: with that option enabled in non-interactive shells (enabled by default since 5.0), $COLUMNS/$LINES are not set until a child process has been waited for and exited (the NEWS entry in the source mentions after a foreground job exits, which is a bit misleading given that there's no job control by default in non-interactive shells). So you need to make sure an external command or subshell has been run synchronously before using those variables:

#! /bin/bash -
shopt -s checkwinsize # for versions 4.3 and 4.4
(:) # start a synchronous subshell that runs the null command
echo "$COLUMNS $LINE"

Also note that it only happens if stderr goes to the terminal (and if not, $COLUMNS remains unset), so you may want to use something like ${COLUMNS:-80} to use a saner default when bash can't determine the screen width.

Alternatively, you could switch to zsh which always sets $COLUMNS even when non-interactive as long as it's running in a terminal (and $COLUMNS defaults to 80 otherwise) or, in any Bourne-like shell use ${COLUMNS:=$(tput cols)} in place of $COLUMNS for $COLUMNS to be set from the output of tput cols if it was previously unset or empty.

If tput cols doesn't work on your system, you can try </dev/tty stty size | awk '{print $2}', or zsh -c 'print $COLUMNS'

Beware however that once $COLUMNS has been set in that way, it won't get updated whenever the terminal is resized¹, so you may want to use $(tput cols) always instead so the terminal size is queried every time you print centred text in your script.

Also beware that printf '%*s' in shells other than zsh and fish pads text to the given number of bytes not characters, so that approach can only be used to pad text containing single byte, single width characters which in locales using UTF-8 is limited to the US-ASCII ones (0.011% of all possible characters).

If using zsh instead of bash, you could use its left and right padding parameter expansion flags instead (which can even handle zero-width or double-width characters with the m flag):

print -r -- ${(ml[COLUMNS/2]r[COLUMNS-COLUMNS/2])fname}

Note that it pads both to the left and the right (so to the right edge of the screen). You can remove the right padding (along with all trailing whitespace) with:

set -o extendedglob
print -r -- ${${(ml[COLUMNS/2]r[COLUMNS-COLUMNS/2])fname}%%[[:space:]]#}

Centring text that contains colouring / bold / standout... escape sequences would be more complicated. Easiest would probably be to remove them before getting the width of the string. For instance, with zsh, using that approach to determine string width (and handling 0-width or double-width characters).

varwidth() (( ${(P)#1} * 3 - ${#${(ml[${(P)#1} * 2])${(P)1}}} ))
functions -Ms varwidth

varwidth_without_formatting() { set -o localoptions -o extendedglob local without_formatting=${(P)1//$'\e'[[0-9;]#m} (( varwidth(without_formatting) )) } functions -Ms varwidth_without_formatting

center() { local text for text do print -r -- ${(l[(COLUMNS-varwidth_without_formatting(text))/2])}$text done }

center $'\e[31mred\e[1;39mbold\e[m'
${(%):-%F{green}Blah%F{yellow}blah%F{magenta}blah%f}


¹ though on most systems you could install a handler for the SIGWINCH signal as @zevzek showed in comments which would help in the most common cases.

2

Instead of the variable COLUMNS you can try to get the value from external source:

tput cols

stty size | cut '-d ' -f1

Romeo Ninov
  • 17,484
1

Try the following:

read WindowHeight WindowWidth<<<$(stty size)
printf "%$(((${#fname}+${WindowWidth})/2))s" "$fname"

COLUMNS is not set automatically in script, so good chois is to use stty to get current window size. this will work in multiple shells (including bash, ksh, zsh)

Saboteur
  • 186
  • 5
  • Neither <<< nor stty size are POSIX though (<<< comes from zsh, stty size will be in the next POSIX version though). Note that read splits words according to the current value of $IFS, so the above assumes $IFS still contains space or whatever character stty uses in your locale to separate the numbers (POSIX will require it being one classified as blank), and no digit. – Stéphane Chazelas Nov 02 '21 at 13:29
  • I am not sure that <<< comes from zsh, it available in bash and it is just a short version of document here construction "https://www.gnu.org/software/bash/manual/html_node/Redirections.html" Maybe I should replace "posix" with non-bash construction, as it is available not only in bash and do the things in script and out of them. – Saboteur Nov 02 '21 at 16:26