printf 字段宽度不支持多字节字符?



我希望printf在计算字段宽度时识别多字节字符,以便列正确排列…我找不到这个问题的答案,想知道这里是否有人有任何建议,或者可能是一个处理这个问题的函数/脚本。

下面是一个简单的例子:
printf "## %5s %5s %5s ##n## %5s %5s %5s ##n" '' '*' '' '' "•" ''>##           *       ##>##         •       ##

显然,我想要的结果:

>##           *       ##>##           •       ##

有什么办法做到这一点吗?

我能想到的最好的是:

function formatwidth
{
  local STR=$1; shift
  local WIDTH=$1; shift
  local BYTEWIDTH=$( echo -n "$STR" | wc -c )
  local CHARWIDTH=$( echo -n "$STR" | wc -m )
  echo $(( $WIDTH + $BYTEWIDTH - $CHARWIDTH ))
}
printf "## %5s %*s %5s ##n## %5s %*s %5s ##n" 
    '' $( formatwidth "*" 5 ) '*' '' 
    '' $( formatwidth "•" 5 ) "•" ''

您使用*宽度说明符将宽度作为参数,并通过在多字节字符中添加额外字节数来计算所需的宽度。

请注意,在GNU wc中,-c返回字节,而-m返回(可能是多字节)字符。

我可能会使用GNU awk:

awk 'BEGIN{ printf "## %5s %5s %5s ##n## %5s %5s %5s ##n", "", "*", "", "", "•", "" }'
##           *       ##
##           •       ##

您甚至可以在awk上编写名为printf的shell包装器函数来保持相同的接口:

tr2awk() { 
    FMT="$1"
    echo -n "gawk 'BEGIN{ printf "$FMT""
    shift
    for ARG in "$@"
        do echo -n ", "$ARG""
    done
    echo " }'"
}

,然后用简单的函数覆盖printf:

printf() { eval `tr2awk "$@"`; }
测试:

# buggy printf binary test:
/usr/bin/printf "## %5s %5s %5s ##n## %5s %5s %5s ##n" '' '*' '' '' "•" ''
##           *       ##
##         •       ##
# buggy printf shell builin test:
builtin printf "## %5s %5s %5s ##n## %5s %5s %5s ##n" '' '*' '' '' "•" ''
##           *       ##
##         •       ##
# fixed printf function test:
printf "## %5s %5s %5s ##n## %5s %5s %5s ##n" '' '*' '' '' "•" ''
##           *       ##
##           •       ##

像python这样的语言可能会以一种更简单、更可控的方式解决你的问题…

#!/usr/bin/python
# coding=utf-8
import sys
import codecs
import unicodedata
out = codecs.getwriter('utf-8')(sys.stdout)
def width(string):
    return sum(1+(unicodedata.east_asian_width(c) in "WF")
        for c in string)
a1=[u'する', u'します', u'trazan', u'した', u'しました']
a2=[u'dipsy', u'laa-laa', u'banarne', u'po', u'tinky winky']
for i,j in zip(a1,a2):
    out.write('%s %s: %sn' % (i, ' '*(12-width(i)), j))

一个纯shell解决方案

right_justify() {
        # parameters: field_width string
        local spaces questions
        spaces=''
        questions=''
        while [ "${#questions}" -lt "$1" ]; do
                spaces=$spaces" "
                questions=$questions?
        done
        result=$spaces$2
        result=${result#"${result%$questions}"}
}

请注意,这仍然不能在破折号中工作,因为破折号没有语言环境支持。

这是有点晚了,但我刚刚看到这个,并认为我将把它发布给其他人遇到同样的帖子。@ninjalj的答案的一个变体可能是创建一个函数,返回给定长度的字符串,而不是计算所需的格式长度:

#!/bin/bash
function sized_string
{
        STR=$1; WIDTH=$2
        local BYTEWIDTH=$( echo -n "$STR" | wc -c )
        local CHARWIDTH=$( echo -n "$STR" | wc -m )
        FMT_WIDTH=$(( $WIDTH + $BYTEWIDTH - $CHARWIDTH ))
        printf "%*s" $FMT_WIDTH $STR
}
printf "[%s]n" "$(sized_string "abc" 20)"
printf "[%s]n" "$(sized_string "ab•cd" 20)"
输出:

[                 abc]
[               ab•cd]

下面是另一个使用(g)awk的解决方案:

function multibyte_printf {
    begin_rule='BEGIN { printf'
    vars=()
    
    for (( arg_index=1; arg_index<=$#; arg_index++ )); do
        begin_rule+=" arg${arg_index},"
        arg="${!arg_index}"
        vars+=('-v' "arg${arg_index}=${arg}")
    done
    
    # Remove last ','
    begin_rule="${begin_rule:0:${#begin_rule}-1}"
    begin_rule+=' }'
    
    gawk "${vars[@]}" "$begin_rule"
}

生成并执行如下命令:

gawk -v 'arg1=%10s' -v 'arg2=World' 'BEGIN { printf arg1, arg2 }'

此解决方案相对于@ michaowŠrajer的主要优点是提高了安全性。使用awk变量而不是将参数放入规则代码中,消除了转义特殊字符的需要。使用不正确的参数应该是不可能篡改执行的。

这是唯一的方法吗?没有办法单独使用printf吗?

用ninjalj的例子,我写了一个脚本来处理这个问题,并将其保存为/usr/local/bin中的fprintf:

#! /bin/bash
IFS=' '
declare -a Text=("${@}")
## Skip the whole thing if there are no multi-byte characters ##
if (( $(echo "${Text[*]}" | wc -c) > $(echo "${Text[*]}" | wc -m) )); then
    if echo "${Text[*]}" | grep -Eq '%[#0 +-]?[0-9]+(.[0-9]+)?[sb]'; then
        IFS=$'n'
        declare -a FormatStrings=($(echo -n "${Text[0]}" | grep -Eo '%[^%]*?[bs]'))
        IFS=$' tn'
        declare -i format=0
    ## Check every format string ##
        for fw in "${FormatStrings[@]}"; do
            (( format++ ))
            if [[ "$fw" =~ ^%[#0 +-]?[1-9][0-9]*(.[1-9][0-9]*)?[sb]$ ]]; then
                (( Difference = $(echo "${Text[format]}" | wc -c) - $(echo "${Text[format]}" | wc -m) ))
            ## If multi-btye characters ##
                if (( Difference > 0 )); then
                ## If a field width is entered then replace field width value ##
                    if [[ "$fw" =~ ^%[#0 +-]?[1-9][0-9]* ]]; then
                        (( Width = $(echo -n "$fw" | gsed -re 's|^%[#0 +-]?([1-9][0-9]*).*[bs]|1|') + Difference ))
                        declare -a Text[0]="$(echo -n "${Text[0]}" | gsed -rne '1h;1!H;${g;y|n|x1C|;s|(%[^%])|n1|g;p}' | gsed -rne $(( format + 1 ))'s|^(%[#0 +-]?)[1-9][0-9]*|1'${Width}'|;1h;1!H;${g;s|n||g;y|x1C|n|;p}')"
                    fi
                ## If a precision is entered then replace precision value ##
                    if [[ "$fw" =~ .[1-9][0-9]*[sb]$ ]]; then
                        (( Precision = $(echo -n "$fw" | gsed -re 's|^%.*.([1-9][0-9]*)[sb]$|1|') + Difference ))
                        declare -a Text[0]="$(echo -n "${Text[0]}" | gsed -rne '1h;1!H;${g;y|n|x1C|;s|(%[^%])|n1|g;p}' | gsed -rne $(( format + 1 ))'s|^(%[#0 +-]?([1-9][0-9]*)?).[1-9][0-9]*([bs])|1.'${Precision}'3|;1h;1!H;${g;s|n||g;y|x1C|n|;p}')"
                    fi
                fi
            fi
        done
    fi
fi
printf "${Text[@]}"
exit 0

用法:fprintf "## %5s %5s %5s ##n## %5s %5s %5s ##n" '' '*' '' '' '•' ''

注意事项:

  • 我没有编写这个脚本来处理格式的*(星号)值,因为我从不使用它们。我写这个是为了我自己,不想让事情变得过于复杂。
  • 我写这个只检查格式字符串%s%b,因为它们似乎是唯一受此问题影响的。因此,如果有人设法从一个数字中获得一个多字节的unicode字符,如果不做一些修改,它可能无法工作。
  • 该脚本非常适合printf的基本使用(而不是一些老的UNIX黑客),请随意修改,或全部使用!

最新更新