0

Here implementation of similar function but for which index position is specified from the start of the string.

How looks implementation of general function for which index position can be specified from the end of the string?

Example with input parameters and expected result for the required function / procedure:

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"

Related question:

This question is part of the project related to my another question: How to retrieve text from the current line at specified cursor position before and after up to specified boundary characters?

2
  • 2
    Try simply: echo ${string::-index_position}$string_to_insert${string: -index_position} Commented Jun 29, 2024 at 17:51
  • @F.Hauri-GiveUpGitHub As I already mentioned such approach in not convenient because requires code duplication. And this is the reason why I created this and related questions: to show the approach without such duplication, by means of a function. Commented Jul 2, 2024 at 11:33

2 Answers 2

4

1. Use bash parameter expansion:

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

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

... Or even:

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

More informations about this in the bash's man page, in Parameter Expansion section search for "Substring Expansion":

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

1.1 As a function

If you want this as a function, regarding the first link shown in you request which stand for same request, but from start of string instead of end, I would suggest to simply use same convention than used in , where using negative position mean from end of string, so we just have to write one function for both operations:

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!

Then if you try:

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

You will see:

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. Same, but for POSIX shell:

In POSIX sh, string indexing is undefined.

So for doing this we have to find another way.

2.1 POSIX shell way using sed:

Using only one fork to 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
}

Will work exactly same:

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 Pure shell without fork

But with loops! Suitable for small strings as cited in you sample.

Under , we could use either Remove matching prefix pattern or Remove matching suffix pattern, depending on which operation (from start or from end) are to be done.

A little condensed for quick display here, on 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
}

Then again:

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
Sign up to request clarification or add additional context in comments.

8 Comments

In this solution "$string" and "$index_position" need to be specified twice. I more prefer approach with functions for reducing code duplication. It is easier to use and simpler to support.
Added function contains: border and other checks, support specification of position from start and from end of the string with convenient convention as used in the very bash, has short intuitive name. And even returning result can be specified as custom var name. This is very professional approach!
Note that printf -v variable is an extension compared to the POSIX command printf.
@JonathanLeffler Yes, but declare -n is a bashism too and not the only! Anyway, I've posted a version tested with recent version of busybox, dash, ksh and bash.
In first,local is not POSIX. But even under bash I use normalized variable names, in order to prevent conflicts.
|
0
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"

To run test:

./insert_string.sh

No output with errors means that test have passed successfully.

1 Comment

Your main function blithely assumes it was called with enough arguments. If you are going to check the arguments at all, surely you should also check that they exist when required.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.