Search code examples
stringbashlinecontainsbats-core

Bash, lines of a variable contain substring?


While trying to write a robust bash method that detects whether a variable that consists of multiple lines of text, contains a substring (that may contain spaces) I am experiencing some difficulties. Three functions were written and I wrote some tests for them using the BATS framework. The functions and the accompanying tests on which they fail, are included below.

Method I

lines_contain_string() {
    local substring="$1"
    shift
    local lines=("$@")
    
    local found_substring="NOTFOUND"

    for i in "${lines[@]}"; do
        if [[ $i == *"$substring"* ]]; then
            echo "FOUND"
            local found_substring="FOUND"
        fi
    done

    if [ "$found_substring" == "NOTFOUND" ]; then
        echo "NOTFOUND"
    fi
}

This fails on test:

EXAMPLE_LINES=$(cat <<-END
First line
second line 
third line 
sometoken
END
)

@test "Substring in first line is found in lines by lines_contain_string." {
    contained_substring="First line"
    
    actual_result=$(lines_contain_string "$contained_substring" "\${EXAMPLE_LINES}")
    #actual_result=$(lines_contain_string_with_space "$contained_substring" "\${EXAMPLE_LINES}")
    EXPECTED_OUTPUT="FOUND"
        
    assert_equal "$actual_result" "$EXPECTED_OUTPUT"
}

So the first method does not catch all substrings.

Method II

And for the second method:

lines_contain_string_with_space() {
    local substring="$1"
    local lines="$@"
    if [ "$lines" == "" ]; then
        echo "NOTFOUND"
    # shellcheck disable=SC2154
    elif [[ "$lines" =~ "$substring" ]]; then
        echo "FOUND"; 
    else
        echo "NOTFOUND";
    fi
}

This seems to return false positive/seems to always return "FOUND", for example, on test:

EXAMPLE_LINES=$(cat <<-END
First line
second line 
third line 
sometoken
END
)

@test "lines_contain_string returns NOTFOUND on non-existing substring." {
    contained_substring="Non-existing-substring"
    
    #actual_result=$(lines_contain_string "$contained_substring" "\${EXAMPLE_LINES}")
    actual_result=$(lines_contain_string_with_space "$contained_substring" "\${EXAMPLE_LINES}")
    EXPECTED_OUTPUT="NOTFOUND"
        
    assert_equal "$actual_result" "$EXPECTED_OUTPUT"
}

Method III

This method is provided by choroba:

string_in_lines() {
    local substring=$1
    shift
    local lines=$1
    if [[ $lines = *"$substring"* ]] ; then
        echo FOUND
    else
        echo NOTFOUND
    fi
}

And calling it with test:

@test "Substring in first line is found in lines by lines_contain_string." {
    contained_substring="First line"
    
    read -p "EXAMPLE_LINES=$EXAMPLE_LINES"

    #actual_result=$(lines_contain_string "$contained_substring" "\${EXAMPLE_LINES}")
    #actual_result=$(lines_contain_string_with_space "$contained_substring" "\${EXAMPLE_LINES}")
    actual_result=$(string_in_lines "$contained_substring" "\${EXAMPLE_LINES}")
    EXPECTED_OUTPUT="FOUND"
        
    assert_equal "$actual_result" "$EXPECTED_OUTPUT"
}

Produces output:

 ✗ Substring in first line is found in lines by lines_contain_string.
   (from function `assert_equal' in file test/no_server_required/preserves_server/../../libs/bats-assert/src/assert_equal.bash, line 40,
    in test file test/no_server_required/preserves_server/test_parsing.bats, line 35)
     `assert_equal "$actual_result" "$EXPECTED_OUTPUT"' failed
   EXAMPLE_LINES=First line
   second line 
   third line 
   sometoken
   -- values do not equal --
   expected : FOUND
   actual   : NOTFOUND
   --

Question

How can one echo "FOUND"/"NOTFOUND" in a bash function, based on whether an incoming variable, consisting of multiple lines, contains a substring that may contain spaces? (And test it accordingly.)

Note

Since my tests also fail on the method provided by choroba, which I assume has a large likelihood of being successful, I expect my test functions are not correct. I updated the question accordingly.

The comments by choroba suggested the backslash in: `"${EXAMPLE_LINES}" was incorrect. I tested whether that was incorrect, and it turned out it was incorrect. So the solution was to write the test as:

@test "Substring in first line is found in lines by lines_contain_string." {
    contained_substring="First line"

    #actual_result=$(lines_contain_string "$contained_substring" "${EXAMPLE_LINES}")
    #actual_result=$(lines_contain_string_with_space "$contained_substring" "${EXAMPLE_LINES}")
    actual_result=$(string_in_lines "$contained_substring" "${EXAMPLE_LINES}")
    EXPECTED_OUTPUT="FOUND"
        
    assert_equal "$actual_result" "$EXPECTED_OUTPUT"
}

@test "lines_contain_string returns NOTFOUND on non-existing substring." {
    contained_substring="Non-existing-substring"
    
    #actual_result=$(lines_contain_string "$contained_substring" "${EXAMPLE_LINES}")
    #actual_result=$(lines_contain_string_with_space "$contained_substring" "${EXAMPLE_LINES}")
    actual_result=$(string_in_lines "$contained_substring" "${EXAMPLE_LINES}")
    EXPECTED_OUTPUT="NOTFOUND"
        
    assert_equal "$actual_result" "$EXPECTED_OUTPUT"
}

Solution

  • Use = with a wildcard pattern.

    string_in_lines() {
        local substring=$1
        shift
        local lines=$1
        if [[ $lines = *"$substring"* ]] ; then
            echo FOUND
        else
            echo NOTFOUND
        fi
    }