Как вставить строку (или символ) в другую строку в указанной индексной позиции с конца в bash?

Здесь реализация аналогичной функции, но для которой позиция индекса указывается с начала строки.

Как выглядит реализация общей функции, для которой позиция индекса может быть указана с конца строки?

Пример с входными параметрами и ожидаемым результатом для требуемой функции/процедуры:

string: "2278"
string_to_insert: "."
index_position: 3

# After inserting "." to 3-rd index position from the end of "string" result should be:
expected_result = "2.278"

Связанный вопрос:

Этот вопрос является частью проекта, связанного с другим моим вопросом: Как получить текст из текущей строки в указанной позиции курсора до и после указанных граничных символов?

Попробуйте просто: echo ${string::-index_position}$string_to_insert${string: -index_position}

F. Hauri - Give Up GitHub 29.06.2024 19:51

@F.Hauri-GiveUpGitHub Как я уже упоминал, такой подход неудобен, поскольку требует дублирования кода. И именно поэтому я создал этот и связанные с ним вопросы: показать подход без такого дублирования, с помощью функции.

Anton Samokat 02.07.2024 13:33
В чем разница между методом "==" и equals()
В чем разница между методом "==" и equals()
Это один из наиболее часто задаваемых вопросов новичкам на собеседовании. Давайте обсудим его на примере.
Замена символа по определенному индексу в JavaScript
Замена символа по определенному индексу в JavaScript
В JavaScript существует несколько способов заменить символ в строке по определенному индексу.
0
2
99
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

cat insert_string.sh

#!/bin/bash

insert_at_position_from_end() {
    local string=$1; shift
    local string_to_insert=$1; shift
    local index=$1; shift
    declare -n out_result=$1; shift

    local length=${#string}

    if (( $index < 0 || $index > $length )); then
        error_msg = "index out of bounds error: index=$index, string length=$length; expected indexes: [0..$length]" 
        out_result = ""
        echo "$error_msg" >&2
        return 2  # exit 2 # for intruppting the script
    fi

    local left_part=${string:0:$length-$index}
    local right_part=${string:$length-$index:$index}
    out_result = "${left_part}${string_to_insert}${right_part}"
}

capture_stderr() {
  local -n stderr = "${1:?}"; shift
  { stderr = "$({ "$@" 1>&3; } 2>&1)"; } 3>&1
}

assert_equals() {
    local error_message=$1; shift
    local expected_result=$1; shift
    local actual_result=$1; shift

    if [ "$expected_result" != "$actual_result" ]; then
        echo "$error_message; Expected: $expected_result; Actual: $actual_result"
    fi
}

test_insert_at_position_from_end() {
    # insert to the middle
    local test_string = "2278"
    local expected = "2.278"
    local actual = ""
    insert_at_position_from_end "$test_string" "." 3 actual
    assert_equals "test_insert_at_position_from_end-10" "$expected" "$actual"

    # insert to the beginning
    local test_string = "278"
    local expected = ".278"
    local actual = ""  # reset from previous test
    insert_at_position_from_end "$test_string" "." 3 actual
    assert_equals "test_insert_at_position_from_end-40" "$expected" "$actual"

    # insert to the end
    local test_string = "278"
    local expected = "278."
    local actual = ""
    insert_at_position_from_end "$test_string" "." 0 actual
    assert_equals "test_insert_at_position_from_end-50" "$expected" "$actual"

    # try to insert out of the right border
    local test_string = "278"
    local actual = ""
    local expected_stderr = "index out of bounds error: index=-1, string length=3; expected indexes: [0..3]"
    capture_stderr actual_stderr insert_at_position_from_end "$test_string" "." -1 actual
    assert_equals "test_insert_at_position_from_end-70" "$expected_stderr" "$actual_stderr"

    # try to insert out of the left border
    local test_string = "278"
    local actual = ""
    local expected_stderr = "index out of bounds error: index=4, string length=3; expected indexes: [0..3]"
    capture_stderr actual_stderr insert_at_position_from_end "$test_string" "." 4 actual
    assert_equals "test_insert_at_position_from_end-110" "$expected_stderr" "$actual_stderr"
}

echo "test started"

test_insert_at_position_from_end

echo "test finished"

Чтобы запустить тест:

./insert_string.sh

Отсутствие вывода с ошибками означает, что тест пройден успешно.

Ваша основная функция беспечно предполагает, что она была вызвана с достаточным количеством аргументов. Если вы вообще собираетесь проверять аргументы, вам обязательно следует также проверить их существование, когда это необходимо.

Jonathan Leffler 29.06.2024 17:32
Ответ принят как подходящий

1. Используйте расширение параметра bash:

string = "2278"
string_to_insert = "."
index_position=3

result=${string::-index_position}$string_to_insert${string: -index_position}
echo $result
2.278

... Или даже:

if (( index_position <= ${#string} )); then
    result=${string::-index_position}$string_to_insert${string: -index_position}
    echo $result
else
    echo "Error string '$string' too small"
fi

Дополнительную информацию об этом можно найти на странице руководства bash, в разделе «Расширение параметров» найдите «Расширение подстроки»:

man -Pless\ '+/^\ *Parameter\ *Expansion\|Substring\ *Expansion' bash

1.1 Как функция

Если вы хотите, чтобы это было как функция, относительно первой ссылки, показанной в вашем запросе, которая обозначает тот же запрос, но с начала строки, а не с конца, я бы предложил просто использовать то же соглашение, что и в bash, где используется отрицательный позиция означает от конца строки, поэтому нам просто нужно написать одну функцию для обеих операций:

insertAt() { 
    if [[ $1 == -v ]]; then
        local -n __iAt_result=$2
        local __iAt_setVar=true
        shift 2
    else
        local __iAt_result __iAt_setVar=false
    fi
    (( $# != 3 )) &&
        printf 'Usage: %s [-v <varname>] <source string> <string to insert> <[-]position>\n' \
          "${FUNCNAME[0]}" >&2 &&
        return 1
    case ${3#-} in *[^0-9]* )
        echo 'Error: 3rd argument must be an integer!' >&2
        return 1 ;;
    esac
    local __iAt_string=$1 __iAt_toInsert=$2 __iAt_position=$3
    (( ${__iAt_position#-} > ${#__iAt_string} )) &&
        echo 'Error: position out of string length' >&2 &&
        return 1
    printf -v __iAt_result '%s%s%s' "${__iAt_string::__iAt_position}" \
          "$__iAt_toInsert" "${__iAt_string: __iAt_position}"
    $__iAt_setVar || echo "$__iAt_result"
}
insertAt 314159265 . 1
3.14159265
string = "Hell!"
insertAt -v string "$string" 'o world' -1
echo "$string"
Hello world!
insertAt 314159265 1 .
Error: 3rd argument must be an integer!

Тогда, если вы попробуете:

for i in {-5..5};do
    unset result
    insertAt -v result 2283 . $i
    [[ -v result ]] && printf '%5d: %s\n' $i "$result"
done

Ты увидишь:

Error: position out of string length
   -4: .2283
   -3: 2.283
   -2: 22.83
   -1: 228.3
    0: .2283
    1: 2.283
    2: 22.83
    3: 228.3
    4: 2283.
Error: position out of string length

2. То же, но для POSIX shell:

В POSIX sh индексация строк не определена.

Поэтому для этого нам придется найти другой путь.

2.1 POSIX shell способ использования sed:

Используя только одну вилку, можно sed:

insertAt() {
    if [ "$1" = "-v" ]; then
        __iAt_setVar=true __iAt_result=$2
        shift 2
    else
        __iAt_setVar=false __iAt_result=__iAt_result
    fi
    [ $# -ne 3 ] && echo >&2 \
        'Usage: insertAt [-v <varname>] <source string> <string to insert> <[-]position>' &&
        return 1
    case ${3#-} in
        *[!0-9]*|'' )
            echo 'Error: 3rd argument must be an integer!' >&2
            return 1 ;;
    esac
    [ "${3#-}" -gt ${#1} ] &&
       echo 'Error: position out of string length' >&2 &&
       return 1
    case $3 in
        -* )
            read -r "${__iAt_result?}" <<EOInLine__IAt_Result
$(echo "$1" | sed -e "s/.\{${3#-}\}$/${2}&/")
EOInLine__IAt_Result
            ;;
        * )
            read -r "${__iAt_result?}" <<EOInLine__IAt_Result
$(echo "$1" | sed -e "s/^.\{$3\}/&${2}/")
EOInLine__IAt_Result
            ;;
    esac
    if ! $__iAt_setVar; then echo "$__iAt_result"; fi
}

Будет работать точно так же:

insertAt 314159265 . 1
3.14159265
string = "Hell!"
insertAt -v string "$string" 'o world' -1
echo "$string"
Hello world!
insertAt 314159265 1 .
Error: 3rd argument must be an integer!
for i in $(seq -- -5 5); do
    unset result
    insertAt -v result 2283 . "$i"
    [ -n "$result" ] && printf '%5d: %s\n' "$i" "$result"
done
Error: position out of string length
   -4: .2283
   -3: 2.283
   -2: 22.83
   -1: 228.3
    0: .2283
    1: 2.283
    2: 22.83
    3: 228.3
    4: 2283.
Error: position out of string length

2.2 Чистый shell без вилки

Но с петлями! Подходит для небольших струн, как указано в вашем образце.

В разделе posix оболочки мы могли бы использовать либо Удалить соответствующий шаблон префикса, либо Удалить соответствующий шаблон суффикса, в зависимости от того, какую операцию (с начала или с конца) необходимо выполнить.

Немного сжато для быстрого отображения здесь, на SO:

insertAt() {
    if [ "$1" = "-v" ]; then __iAt_setVar=true __iAt_result=$2; shift 2
    else __iAt_setVar=false __iAt_result=__iAt_result; fi
    [ $# -ne 3 ] && echo >&2 \
        'Usage: insertAt [-v <varname>] <source string> <string to insert> <[-]position>' &&
        return 1
    case ${3#-} in *[!0-9]*|'' )
        echo 'Error: 3rd argument must be an integer!' >&2
        return 1 ;; esac
    [ "${3#-}" -gt ${#1} ] &&
       echo 'Error: position out of string length' >&2 &&
       return 1
    __iAt_var1 = "" __iAt_var2 = "" __iAt_cnt=$(( ${#1} - ${3#-} ))
    while [ $__iAt_cnt -gt 0 ]; do
           __iAt_var1 = "?$__iAt_var1" __iAt_cnt=$((__iAt_cnt-1))
    done
    __iAt_cnt=$(( ${3#-} ))
    while [ $__iAt_cnt -gt 0 ]; do
           __iAt_var2 = "?$__iAt_var2" __iAt_cnt=$((__iAt_cnt-1))
    done
    case $3 in -* ) 
            read -r "${__iAt_result?}" <<EOInLine__IAt_Result
${1%$__iAt_var2}$2${1#$__iAt_var1}
EOInLine__IAt_Result
            ;; * )
            read -r "${__iAt_result?}" <<EOInLine__IAt_Result
${1%$__iAt_var1}$2${1#$__iAt_var2}
EOInLine__IAt_Result
            ;; esac
    if ! $__iAt_setVar; then echo "$__iAt_result"; fi
}

Затем снова:

insertAt 314159265 . 1
3.14159265
string = "Hell!"
insertAt -v string "$string" 'o world' -1
echo "$string"
Hello world!
insertAt 314159265 1 .
Error: 3rd argument must be an integer!
for i in $(seq -- -5 5); do
    unset result
    insertAt -v result 2283 . "$i"
    [ -n "$result" ] && printf '%5d: %s\n' "$i" "$result"
done
Error: position out of string length
   -4: .2283
   -3: 2.283
   -2: 22.83
   -1: 228.3
    0: .2283
    1: 2.283
    2: 22.83
    3: 228.3
    4: 2283.
Error: position out of string length

В этом решении «$string» и «$index_position» необходимо указать дважды. Я больше предпочитаю подход с функциями для уменьшения дублирования кода. Его проще использовать и проще поддерживать.

Anton Samokat 29.06.2024 22:45

Добавленная функция содержит: границу и другие проверки, поддерживает указание позиции от начала и до конца строки с удобным соглашением, используемым в самом bash, имеет короткое интуитивное имя. И даже возвращаемый результат можно указать как собственное имя переменной. Это очень профессиональный подход!

Anton Samokat 30.06.2024 14:10

Обратите внимание, что printf -v variable является расширением по сравнению с командой POSIX printf.

Jonathan Leffler 30.06.2024 16:27

@JonathanLeffler Да, но declare -n — это тоже башизм, и не единственный! В любом случае, я опубликовал версию, протестированную с последними версиями busybox , Dash , ksh и bash.

F. Hauri - Give Up GitHub 30.06.2024 19:54

@AntonSamokat Рассмотрите возможность принятия ответа, если он соответствует вашему запросу!

F. Hauri - Give Up GitHub 02.07.2024 07:55

Я буду. Но нам нужно уделить немного времени другим. На случай, если они тоже захотят поделиться своим опытом.

Anton Samokat 02.07.2024 13:25

Почему бы не использовать local вместо префикса __iAt_ для переменных?

Anton Samokat 02.07.2024 13:26

Во-первых,local — это не POSIX. Но даже в bash я использую нормализованные имена переменных, чтобы предотвратить конфликты.

F. Hauri - Give Up GitHub 02.07.2024 15:02

Другие вопросы по теме