0

What's an idiomatic way to test if a string contains a substring in a Posix Shell?

Basically this, but Posix:

[[ ${my_haystack} == *${my_needle}* ]]

Non-Posix Example

I'm looking for the equivalent of this, but that works in a Posix / Almquist / dash / ash shell:

#!/bin/bash

set -e
set -u

find_needle() {
    my_haystack="${1}"
    my_needle="${2}"

    if [[ ${my_haystack} == *${my_needle}* ]]; then
        echo "'${my_haystack}' contains '${my_needle}'"
    else
        echo "'${my_haystack}' does NOT contain '${my_needle}'"
    fi
}

find_needle "${1:-"haystack"}" "${2:-"a"}"

(that doesn't work in sh)

My ideal solution would be one that doesn't require the use of a subshell or pipe, and that doesn't exit on failure in strict mode.

Workaround

This works, but I'm wondering if there's another way to test a substring without echoing and piping to grep.

#!/bin/sh

set -e
set -u

find_needle() {
    my_haystack="${1}"
    my_needle="${2}"
    if echo "${my_haystack}" | grep -q "${my_needle}"; then
        echo "'${my_haystack}' contains '${my_needle}'"
    else
        echo "'${my_haystack}' does NOT contain '${my_needle}'"
    fi
}

find_needle "${1:-"haystack"}" "${2:-"a"}"

Or maybe this is the most idiomatic way?

coolaj86
  • 74,004
  • 20
  • 105
  • 125
  • 1
    Debatable if it's idiomatic, but `case $haystack in (*$needle*) echo yes;; (*) echo no;; esac` is POSIX. The ( are optional, but I like them. Note `grep` treats 'needle' as a regexp (BRE) with . special but ? literal not special as in shell pattern including bash `[[`, also leading ^ and trailing $ special, and [charlist] similar but not identical. – dave_thompson_085 Aug 19 '22 at 07:22
  • Are you asking for a way without piping or for the most idiomatic way? My go-to for POSIX is with grep, but see [this thread](https://stackoverflow.com/q/2829613/10678955). – root Aug 22 '22 at 00:16
  • How did you get the nab @root as a username? That's awesome. I'm root on npm and I tried to get it on github. I envy you. :) Yes, I'm asking for both. – coolaj86 Aug 22 '22 at 15:40

1 Answers1

0

Substring match with case

As @dave_thompson_085 points out [1], you can use case:

case $haystack in
    *$needle*)
        return 0
        ;;
    *)
        return 1
        ;;
esac

See also:

Example Script

  • ✅ no subshell
  • ✅ no pipe
  • ‍♂️ simplest, most idiomatic? maybe
#!/bin/sh

set -e
set -u

test_substring() {
    haystack="${1}"
    needle="${2}"

    case $haystack in
        *$needle*)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

find_needle() {
    my_haystack="${1}"
    my_needle="${2}"
    if test_substring "${my_haystack}" "${my_needle}"; then
        echo "'${my_haystack}' contains '${my_needle}'"
    else
        echo "'${my_haystack}' does NOT contain '${my_needle}'"
    fi
}

find_needle haystack a
find_needle haystack x

Output:

'haystack' contains 'a'
'haystack' does NOT contain 'x'
coolaj86
  • 74,004
  • 20
  • 105
  • 125