오른쪽으로 조정된 문자열(즉, 터미널 오른쪽)을 인쇄하는 함수를 만들고 있습니다. 이를 위해서는 두 가지가 필요합니다.
- 장식된 너비가 0인 모든 부분(색상 등)을 포함하여 문자열을 인쇄하겠습니다.
- 너비가 0인 부분(더 정확하게는 길이)을 모두 제외하고 문자열을 인쇄하겠습니다.
분명한 이유로 오른쪽으로 얼마나 오프셋해야 하는지 알려면 [1]이 필요하고 [2]가 필요합니다. terminal width
-[2]
이를 위해 PS1/PS2와 동일한 형식을 선택했습니다. \[
와 사이에 너비가 0인 섹션을 배치했습니다.\]
perl
예상대로 작동하는 탐욕스럽지 않은 정규식을 사용하여 함수를 작성했습니다 .
# This needs zero-width parts surrounded by \[ and \] just like PS1/PS2
function _print_right_adjusted_perl() {
local escaped_line printed_line nonzero_line forward
#line with all zero-width parts escaped by \[ and \]
escaped_line="${1}"
# [1]: Only the \[ and \] markers removed; this will be the thing that is actually printed.
printed_line="$(perl -pe 's|\\\[(.*?)\\\]|\1|g' <<<"${escaped_line}")"
# [2]: all zero-with parts removed, including the markers \[ and \].
nonzero_line="$(perl -pe 's|\\\[.*?\\\]||g' <<<"${escaped_line}")"
# "carriage return" (literally) returns cursor to the first column of this row
printf "$(tput cr)"
# tput cuf N: move cursor forward N times
forward="$(( "$(tput cols)" - "${#nonzero_line}" ))"
printf "$(tput cuf "${forward}")"
# print the actual text
printf "${printed_line}"
}
_print_right_adjusted "\[$(tput setaf 7)\]my coloured thing"
그런 다음 순수 bash 버전을 작성하기 시작했지만 여전히 문제가 있었습니다.
아래 기능이 내 답변으로 대체되었습니다.
# This needs zero-width parts surrounded by \[ and \] just like PS1/PS2
function _print_right_adjusted_old() {
local printed_string="" has_length='true' first='true' added_part=''
local -i printed_length=0 forward
# split input string at backslashes. eg. turn:
# "normal \[zero-width\]colour string"
# into:
# ( "normal" "[zero-width" "]colour string" )
IFS='\'
for part in ${1}; do
# check what the first character is
case "${part:0:1}" in
'[')
# start of a zero-width section
has_length='false';
# remove the marker
part="${part#'['}"
;;
']')
# end of a zero-width section
has_length='true';
# remove the marker
part="${part#']'}"
;;
# not '\[' or '\]', re-add '\' except for the first segment
*) [[ "${first}" != 'true' ]] && part="\\${part}" ;;
esac
first='false'
printed_string+="${part}"
if [[ "${has_length}" == 'true' ]]; then
printed_length+="${#part}"
fi
done
# cr: "carriage return" (literally) returns cursor to the first column of current row
# cuf N: cursor-forward: move cursor N spaces forward (to the right)
forward="$(( "$(tput cols)" - "${printed_length}" ))"
printf "$(tput cr)$(tput cuf "${forward}")"
# print the actual string
printf "${printed_string}"
}
${variable@P}
PS1과 같은 변수를 해석하는 것보다 마지막 줄을 대체할 수 있고 printf "${1@P}"
해당 줄을 제거할 수 있다는 것을 배웠습니다 .printed_string+="${part}"
답변1
그 이후로 나는 이 스크립트를 두 번 다시 작성했습니다.
첫 번째 버전은 잘 작동하지만 1~2자마다( length/2
와 사이) 반복하기 때문에 속도가 느립니다 length
.
function _print_right_adjusted_2step() {
local escaped_string="${1:?'need PS1-style string as 1st argument'}"
local has_width='y'
local -i length=0 terminal_width="$(tput cols)"
# loop over string character by character and check 2 characters starting at ${i}
for (( i=0; i<"${#escaped_string}"; i++ )); do case "${escaped_string:${i}:2}" in
# when first character is a backslash, check for \[ or \] combination
'\[') ((++i)); has_width='' ; ;;
'\]') ((++i)); has_width='y'; ;;
# when 2nd character is a backslash we go forward 1 character only so the backslash is first on the next loop iteration
?'\') [[ -n "${has_width}" ]] && length+=1; ;;
# when 2nd character is not a backslash we can skip ahead
?*) ((++i)); [[ -n "${has_width}" ]] && length+=2; ;;
esac; done
local offset="$(( "${terminal_width}" - "${length}" ))"
printf "$(tput cr)$(tput cuf "${offset}")${escaped_string@P}"
}
두 번째 버전은 몇 번만 반복하기 때문에 훨씬 빠릅니다. (개수 \
) + 1
function _print_right_adjusted_new() {
local escaped_string="${1:?'need PS1-style string as 1st argument'}"
local prefix='' has_width='y' terminal_width="$(tput cols)" nonzero_string=''
local -i length=0
local IFS='\'
for segment in ${escaped_string}; do
# with the 1st segment, prefix is '' (empty string)
# after the 1st segment, prefix is '\' (a backslash)
case "${prefix}${segment:0:1}" in
# when first character is a backslash, check for \[ or \] combination
'\[') has_width='' ; ;;
# remove the soon-to-be-added ']' from the length
'\]') has_width='y'; ((length--)) ;;
# add the missing '\' to the length
'\'?) ((length++)) ;;
esac
if [[ -n "${has_width}" ]]; then
# use the line below if you need the actual content
# nonzero_string+="${segment}"
length+="${#segment}"
fi
prefix='\'
done
local offset="$(( "${terminal_width}" - "${length}" ))"
printf "$(tput cr)$(tput cuf "${offset}")${escaped_string@P}"
}
그런 다음 인쇄 오버헤드 없이 각 버전을 10,000번 테스트했는데 모든 버전이 동일했습니다.
#!/bin/bash
set -euo pipefail
function _print_right_adjusted_2step() {
local print_start_time="${EPOCHREALTIME/./}"
local has_width='y' escaped_string="${1:?'need PS1-style string as 1st argument'}"
local -i length=0 terminal_width=0 offset=0
# loop over string character by character and include the next character in the check ( 2 characters starting at ${i} )
for (( i=0; i<"${#escaped_string}"; i++ )); do case "${escaped_string:${i}:2}" in
# when first character is a backslash, check for \[ or \] combination
'\[') ((++i)); has_width='' ; ;;
'\]') ((++i)); has_width='y'; ;;
# when 2nd character is a backslash we go forward 1 character only so the backslash is first on the next loop iteration
?'\') [[ -n "${has_width}" ]] && length+=1; ;;
# when 2nd character is not a backslash we can skip ahead
?*) ((++i)); [[ -n "${has_width}" ]] && length+=2; ;;
esac; done
#offset="$(( "${terminal_width}" - "${length}" ))"
#printf "$(tput cr)$(tput cuf "${offset}")${escaped_string@P}"
local print_end_time="${EPOCHREALTIME/./}"
printf >&3 "$(( (print_end_time - print_start_time) ))"
return 0
}
function _print_right_adjusted_new() {
local print_start_time="${EPOCHREALTIME/./}"
local escaped_string="${1:?'need PS1-style string as 1st argument'}"
local prefix='' has_width='y'
local -i length=0
IFS='\'
for segment in ${escaped_string}; do
# loop over string character by character and include the next character in the check ( 2 characters starting at ${i} )
case "${prefix}${segment:0:1}" in
# when first character is a backslash, check for \[ or \] combination
'\[') has_width='' ; ;;
# remove the soon-to-be-added ']' from the length
'\]') has_width='y'; ((length--)) ;;
# add the missing '\' to the length
'\'?) ((length++)) ;;
esac
[[ -n "${has_width}" ]] && length+="${#segment}"
prefix='\'
done
#local offset="$(( "${terminal_width}" - "${length}" ))"
#printf "$(tput cr)$(tput cuf "${offset}")${escaped_string@P}"
local print_end_time="${EPOCHREALTIME/./}"
printf >&3 "$(( (print_end_time - print_start_time) ))"
return 0
}
function _print_right_adjusted_perl() {
local print_start_time="${EPOCHREALTIME/./}"
# line with all zero-width parts escaped by \[ and \]
local escaped_string="${1:?'need PS1-style string as 1st argument'}"
local nonzero_string offset terminal_width=0
# line with all zero-with parts removed, including the markers \[ and \]
nonzero_string="$(perl -pe 's|\\\[.*?\\\]||g' <<<"${escaped_string}")"
#terminal_width="$(tput cols)"
#offset="$(( "${terminal_width}" - "${#nonzero_line}" ))"
# tput cr: "carriage return", literally.
# tput cuf N: move cursor forward N times
#printf "$(tput cr)$(tput cuf "${offset}" )${escaped_string@P}"
local print_end_time="${EPOCHREALTIME/./}"
printf >&3 "$(( (print_end_time - print_start_time) ))"
return 0
}
# https://stackoverflow.com/a/56151840
function sort_with_header() {
sed -u '1q'; sort "${@}"
}
input="normal\[$(tput smul)\]underlined\[$(tput rmul)$(tput bold)$(tput setaf 5)\]color stuff\[$(tput sgr0)\]normal"
printf "starting test\n"
{
printf >&3 "name\tms\n"
for f in $(seq 1 10000); do
#printf '.'
printf >&3 "2step\t"
_print_right_adjusted_2step "${input}"
printf >&3 "\nperl\t"
_print_right_adjusted_perl "${input}"
printf >&3 "\nnew\t"
_print_right_adjusted_new "${input}"
printf >&3 "\n"
done
} 3> timing.txt
printf "\ntesting is done!\n"
<timing.txt datamash -H --sort --group name median 2 mode 2 mean 2 pstdev 2 | sort_with_header -h -k2 | column -t | tee stats.txt
어떤 출력:
GroupBy(name) median(ms) mode(ms) mean(ms) pstdev(ms)
new 58 57 60.9599 8.6375165406499
2step 237 236 245.2142 31.085467961091
perl 1003 997 1011.1479 41.703832265033
나는 새 버전을 성공이라고 부릅니다 :-)