4

I read strings from stdin and I want to display the users that match the strings. Problem is, if the user inputs the character '[', or a string containing it.

grep -F does not work because the line has to start with the string (^ - which is a simple character with -F). Also, getent $user won't be good because I need only the username not id as well.

if [[ "$user" == *"["* ]]; then
    echo -e "Invalid username.\n"
    continue
fi

if ! getent passwd | grep "^$user:"; then
    echo -e "Invalid username.\n"
    continue
fi

This is the workaround for '[', is there another way? awk will do the job most probably, but I have no knowledge of it yet, I'm interested in grep.

Tigrex
  • 53

3 Answers3

9

Either escape it or put it in a character class, something along these lines:

grep '\['

grep '[[]'

grep -e "${user//\[/\\\[}"

The syntax ${var//c/d} => in the shell variable $var we replace all the characters c with d. Now, in your case the c is [ but it so happens that [ is special in this syntax (it does globbing) and hence we need to escape it by prefixing it with a backslash, i.e., \[.

Now coming to the replacement part, what we need is a \[ in there. But again, both \ and [ are special in this syntax of ${var//...} parameter substitution and hence both need to be, yes you guessed it right, backslashed leading to the expression: \\\[ : "${var//\[/\\\[}"

HTH

terdon
  • 242,166
5

[ is not the only character to escape for regexps. All the RE operators are including . which is common in user names (r.ot as a regexp matches root for instance).

Also, your approach (getent passwd | grep "^$user:") is also invalid in that it wouldn't flag root:0 as invalid for instance.

Here, it would be better to use awk:

user_valid() {
  getent passwd | 
    U="$1" awk -F: '$1 == ENVIRON["U"] {found = 1; exit}
                    END {exit(1 - found)}'
}

Now, not all user databases allow enumeration like that.

$ getent passwd | grep stephane
$ id -u stephane
10631
$ getent passwd stephane
stephane:*:10631:10631:Stephane Chazelas:/export/./home/stephane:/bin/zsh

In my case, that user is in a LDAP database. enumeration is disabled (there could be thousands of users), but I can still query/resolve users individually.

So here, to validate users, it's better to query the user database directly for that user. For instance, using the id command (a standard command contrary to getent):

user_valid() {
  case $1 in
    (*[!0-9]*) id -u -- "$1" > /dev/null 2>&1;;
    (*) false;;
  esac
}

(we're taking care of all-digits users separately as some id implementations would give you information for the user id in that case. User names can't be numerical in most systems (that would break most commands that expect either user names or user ids as arguments (like those ids above, ps, find...))).

2

Escaping special characters is a bit of a chore. Instead you could just pick the username field from getent's output first, and then match against the full remaining line:

LC_ALL=C
if ! [[ $user =~ ^[A-Za-z0-9._][A-Za-z._-]*$ ]] ; then
    echo "Username is invalid"
    continue
fi
getent passwd | cut -d: -f1 | grep -xF -e "$user"

-F for fixed-strings, -x for full line match.

If you don't have users with usernames consisting only of digits, you could use getent:

LC_ALL=C
if ! [[ $user =~ ^[A-Za-z0-9._][A-Za-z._-]*$ ]] ; then
    echo "Username is invalid"
    continue
elif [[ $user =~ ^[0-9]+$ ]] ; then
    echo "Cannot handle username of digits only, sorry :("
    continue
fi
if ! getent -- passwd "$user" > /dev/null ; then
    echo "$user doesn't exist"
    continue
fi

Or, to avoid the problems with getent or others assuming a string of digits must be an UID instead of a user name we should call getpwnam() manually. This doesn't make other assumptions of what the usernames can be, other than what the underlying getpwnam() implementation does.

export user
if ! perl -e 'exit !defined getpwnam($ENV{user})' ; then 
    echo "$user doesn't exist"
fi

I'll skip writing the corresponding C wrapper.

ilkkachu
  • 138,973
  • Because of the ID, if I search for user 1 it will give me the user with ID 1, not the user with the name 1. – Tigrex Apr 28 '17 at 08:08
  • @Tigrex, ahh, I didn't remember getent can look up by UID, too. Of course we could just complain if the entered string only contains digits. Technically, you could have a username that wholly consists of digits, but I have a feeling that would get confused with UIDs in more than one place. – ilkkachu Apr 28 '17 at 08:15
  • 1
    @Tigrex: about this, https://unix.stackexchange.com/a/287079/27616 – Olivier Dulac Apr 28 '17 at 13:37