순수 bash의 PS1에서 너비가 0인 섹션 제거

순수 bash의 PS1에서 너비가 0인 섹션 제거

오른쪽으로 조정된 문자열(즉, 터미널 오른쪽)을 인쇄하는 함수를 만들고 있습니다. 이를 위해서는 두 가지가 필요합니다.

  1. 장식된 너비가 0인 모든 부분(색상 등)을 포함하여 문자열을 인쇄하겠습니다.
  2. 너비가 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

나는 새 버전을 성공이라고 부릅니다 :-)

관련 정보