How can I make sure a string contains at least one uppercase letter, one lowercase letter, one number and one punctuation character?
With flexible awk
pattern matching:
if [[ $(echo "$string" | awk '/[a-z]/ && /[A-Z]/ && /[0-9]/ && /[[:punct:]]/') ]]; then
echo "String meets your requirements"
else
echo "String does not meet your requirements"
fi
With one call to awk
and without pipe:
#! /bin/sh -
string='whatever'
has_char_of_each_class() {
LC_ALL=C awk -- '
BEGIN {
for (i = 2; i < ARGC; i++)
if (ARGV[1] !~ "[[:" ARGV[i] ":]]") exit 1
}' "$@"
}
if has_char_of_each_class "$string" lower upper digit punct; then
echo OK
else
echo not OK
fi
That's POSIX but note that mawk
doesn't support POSIX character classes yet. The --
is not needed with POSIX compliant awk
s but would be in older versions of busybox awk
(which would choke on values of $string
that start with -
).
A variant of that function using a case
shell construct:
has_char_of_each_class() {
input=$1; shift
for class do
case $input in
(*[[:$class:]]*) ;;
(*) return 1;;
esac
done
}
Note however that changing the locale for the shell in the middle of a script doesn't work with all sh
implementations (so you'd need the script to be called in the C locale already if you want the input to be considered as being encoded in the C locale charset and the character classes to match only the ones specified by POSIX).
The following script is longer than your code, but shows how you could test a string against a list of patterns. The code detects whether the string matches all patterns or not and prints out a result.
#!/bin/sh
string=TestString1
failed=false
for pattern in '*[[:upper:]]*' '*[[:lower:]]*' '*[[:digit:]]*' '*[[:punct:]]*'
do
case $string in
$pattern) ;;
*)
failed=true
break
esac
done
if "$failed"; then
printf '"%s" does not meet the requirements\n' "$string"
else
printf '"%s" is ok\n' "$string"
fi
The case ... esac
compound command is the POSIX way to test a string against a set of globbing patterns. The variable $pattern
is used unquoted in the test, so that the match is not done as a string comparison. If the string does not match the given pattern, then it will match *
, and the loop is exited after setting failed
to true
.
Running this would yield
$ sh script.sh
"TestString1" does not meet the requirements
You could tuck the testing away in a function like so (the code tests a number of strings in a loop, calling the function):
#!/bin/sh
test_string () {
for pattern in '*[[:upper:]]*' '*[[:lower:]]*' '*[[:digit:]]*' '*[[:punct:]]*'
do
case $1 in ($pattern) ;; (*) return 1; esac
done
}
for string in TestString1 Test.String2 TestString-3; do
if ! test_string "$string"; then
printf '"%s" does not meet the requirements\n' "$string"
else
printf '"%s" is ok\n' "$string"
fi
done
If you want to set LC_ALL=C
locally in the function, write it as
test_string () (
LC_ALL=C
for pattern in '*[[:upper:]]*' '*[[:lower:]]*' '*[[:digit:]]*' '*[[:punct:]]*'
do
case $1 in ($pattern) ;; (*) return 1; esac
done
)
Note that the body of the function now is in a sub-shell. Setting LC_ALL=C
will therefore not affect the value of this variable in the calling environment.
Get the shell function to take the patterns as arguments too, and you basically get Stéphane Chazelas' answer (the variant).