我希望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黑客),请随意修改,或全部使用!