Search code examples
stringbashinsert

How to insert string (or character) into another string at specified index position from the end in bash?


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?


Solution

  • 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