-1

I have this code that checks that the string contains only space and hash characters (“#”), if so it echos 'yes' if not echos 'no'

string="###############
        #             #
        # #############
        #             #
        # ######### # #
        #         # # #
        # ### ##### # #
        #   #     # # #
        # # ###########
        # #            
        ###############"

confirm_variable="Yes" for (( i=0; i<${#string}; i++ )); do str="${string:$i:1}" if [ "$str" == "#" ] || [ "$str" == " " ] || [ "$str" == "\n" ] ; then continue else confirm_variable="No" break fi

done echo $confirm_variable

I am not sure why this isn't working as if I make the string equal something like:

string="## #### # #  #" 

it seems to work fine.

3 Answers3

1

"\n" is the two characters backslash and lowercase letter n.

Now, in some shells, echo "\n", would print a newline (two, actually), and in all shells printf "\n" would. But that's because echo and printf treat the backslash specially. This unlike e.g. C, Perl and Python, where backslash-escapes are always treated specially in (double-quoted) strings.

In many shells, $'\n' would be a string that contains the newline character and nothing more. (But in pure POSIX sh, you'd need a literal newline within quotes.)

That said, in Bash/Ksh/Zsh you could just test the string against a regex instead of manually looping:

re=$'^[# \n]*$'
if [[ $string =~ $re ]]; then
    echo "string contains only #, space and newline"
fi

(for (( .. )) and ${string:i:j} are also not standard POSIX features and won't work in e.g. Dash, which Ubuntu has as /bin/sh.)

ilkkachu
  • 138,973
1

As others have pointed out, "\n" is not a newline, just a string with \ and n in it.

If you want to detect whether the string only has spaces, hashes and newlines in it, use a pattern match rather than going through each individual character.

You can do that like so:

case $string in
    (*[![:space:]#]*)
        echo 'other characters in string'
        ;;
    (*)
        echo 'only space-like characters or hashes in string'
esac

or like so (in bash):

if [[ $string == *[![:space:]#]* ]]; then
        echo 'other characters in string'
else
        echo 'only space-like characters or hashes in string'
fi

The [:space:] thing that I've used here is a POSIX character class that will match all sorts of space-like characters, including space, tabs (of various types), carriage-return, and newline. The pattern *[![:space:]#]* would match any string that contains a character that is not a space-like character, or a #.

Would you want to be more restrictive so that you don't allow tabs or carriage-returns, for example, then use $'*[! \n#]*' as the pattern in bash:

pattern=$'*[! \n#]*'
if [[ $string == $pattern ]]; then
        echo 'other characters in string'
else
        echo 'only spaces, hashes, or newlines in string'
fi

Or, in a standard sh shell:

pattern='*[!
 #]*'

case $string in ($pattern) echo 'other characters in string' ;; (*) echo 'only spaces, hashes, or newlines in string' esac

Kusalananda
  • 333,661
0

You really shouldn't use sh (any variant, from plain old sh to bash or ksh or zsh) to do any but the most trivial string or text processing. See Why is using a shell loop to process text considered bad practice? for some of the reasons why.

Looping over every character of a multi-line string is something that definitely shouldn't be done in shell alone. It'll be horrendously slow, and you'll run into all sorts of issues with quoting and end-of-line markers (like \n), and most versions of sh (except for bash, ksh, and zsh) don't even have built-in support for regular expressions - which is, IMO, minimum required functionality for text processing.

Instead, use awk or perl or some other text-processing utility/language.

For example, you could replace your entire for loop with:

echo "$string" | perl -lne 'BEGIN {$c="Yes"; $ec=0};
                            if (!m/^[ #]+$/) { $c="No"; $ec=1; last };
                            END {print $c; exit $ec}'

or

echo "$string" | awk -v c="Yes" -v ec=0 \
                     '!/^[ #]*$/ { c="No"; ec=1; nextfile };
                      END { print c; exit ec}'

This requires GNU awk (or some other awk that supports nextfile). On other versions, of awk, the following will work - but a tiny bit slower because it doesn't immediately exit the loop on a bad match:

echo "$string" | awk -v c="Yes" -v ec=0 \
                     '!/^[ #]*$/ { c="No"; ec=1 };
                      END { print c; exit ec}'

As should be obvious, these are all the same script, just rewritten slightly differently for the different syntaxes of perl and awk.

The regex used in both perl and awk above is /^[ #]*$/, negated with a !. This matches any line that doesn't contain only spaces or hashes.

All three of them return an exit code of 0 on a confirmed-good input, and 1 on a bad match. This can be tested in sh with the $? variable:

if [ $? -eq 1 ] ; then
   : # string has bad characters, do something!
fi

You could also use a simple GNU sed script:

echo "$string" | sed -n -e '/[^ #]/q1'
if [ $? -eq 0 ] ; then echo "Yes" else echo "No" ; fi

The exit code to the q (quit) command is a GNU extension to sed, so requires GNU sed.

Or, as @Isaac pointed out, you can use grep's -q (or --quiet/--silent) option. This suppresses normal output and only returns an exit code. For example:

echo "$string" | grep -q '[^ #]'
if [ $? -eq 0 ] ; then echo "Yes" else echo "No" ; fi
cas
  • 78,579