Exercise: Parse A Line From /etc/passwd

Problem

/etc/passwd contains lines of the form

jfasch:x:1000:1000:Joerg Faschingbauer:/home/jfasch:/bin/bash

We want to parse such content conveniently in C++ [1]

Requirements

Library Function (lib/parse-passwd.cpp)

Write a function

User parse_passwd_line(std::string line);
  • The parameter line is one line as read from /etc/passwd

  • On success, the function returns an object of type User (definition see below)

  • On failure, the function throws an exception object of type PasswdError (definition see below)

The header file lib/parse-passwd.h contains the type definitions and the function declaration, the implementation file lib/parse-passwd.cpp contains only the function skeleton which does nothing, and which has to be implemented.

Important

lib/parse-passwd.cpp is the only file that must be modified in this exercise!

lib/parse-passwd.h (download)
#pragma once

#include <string>


/** User

    Represents the data from one line of /etc/passwd, e.g.
    "jfasch:x:1000:1000:Joerg Faschingbauer:/home/jfasch:/bin/bash"

    See ``man -s 5 passwd``

 */
struct User
{
    User() : uid(-1), gid(-1) {}    // <--- default constructor

    std::string name;
    std::string passwd;
    int uid;
    int gid;
    std::string descr;
    std::string homedir;
    std::string shell;
};

/** PasswdError

    Exception class to be throw on error from parse_passwd_line() (see
    below)

 */
struct PasswdError
{
    enum Code {                              // <--- nested type definition (error codes)
        LINE_INVALID,
    };

    PasswdError(Code code, std::string msg)  // <--- default constructor
    : code(code), msg(msg) {}

    Code code;                               // <--- struct member
    std::string msg;                         // <--- struct member
};

/** parse_passwd_line

    Given a line, parse from it items of ``struct User``.
    Throw exceptions of type ``PasswdError`` as they are encountered.

    See the test suite for detailed specs.

 */
User parse_passwd_line(std::string line);

Tests (tests/suite-passwd-line.cpp)

The specification of the parse_passwd_line() function is in ./tests/suite-passwd-line.cpp. The other test suites in tests/ are implementation hints (see below).

Do not modify the tests!

Program (bin/read-passwd.cpp)

Finally, after all tests pass, the program built from bin/read-passwd.cpp (this file must not be modified) should give an output similar to this:

$ ./bin/read-passwd /etc/passwd
...
User.name    jfasch
    .passwd  x
    .uid     1000
    .gid     1000
    .descr   Jörg Faschingbauer
    .homedir /home/jfasch
    .shell   /bin/bash
...

Hints

How To Proceed

  • Comment out all tests

  • Build the project

  • In a top-down manner …

    • Comment back in one test after the other

    • Fix parse_passwd_line() until the current succeeds

During the implementation of parse_passwd_line() you will need to use

  • User objects that are returned on success from the function

  • PasswdError objects that are thrown as exceptions from the function

  • Miscellaneous std::string functionality to dissect a line

User Usage (tests/suite-passwd-user.cpp)

parse_passwd_line() returns an object of type User. See below how such objects are initialized and used.

tests/suite-passwd-user.cpp (download)
#include <parse-passwd.h>

#include <gtest/gtest.h>


// how to create a User object
TEST(passwd_user_suite, object_construction_and_member_access)
{
    User user;    // <--- calls default constructor

    // all default-initialized (even the numbers)
    ASSERT_EQ(user.name, "");
    ASSERT_EQ(user.passwd, "");
    ASSERT_EQ(user.uid, -1);
    ASSERT_EQ(user.gid, -1);
    ASSERT_EQ(user.descr, "");
    ASSERT_EQ(user.homedir, "");
    ASSERT_EQ(user.shell, "");
}

PasswdError Usage (tests/suite-passwd-error.cpp)

The exception type PasswdError has a nested enum type, which is a bit hard to use for C++ beginners. See tests/suite-passwd-error.cpp for usage examples - especially how you use the type in a throw statement.

tests/suite-passwd-error.cpp (download)
#include <parse-passwd.h>

#include <gtest/gtest.h>


// how to create an error
TEST(passwd_error_suite, object_construction_and_member_access)
{
    PasswdError error(/*code*/PasswdError::LINE_INVALID, /*msg*/"howdy");

    PasswdError::Code code = error.code;
    std::string msg = error.msg;

    ASSERT_EQ(code, PasswdError::LINE_INVALID);
    ASSERT_EQ(msg, "howdy");
}

// how to throw an error
static User parse_passwd_line__fake(std::string line)
{
    User u;

    // lets say we see an error during parsing the line
    if (false /*error :-)*/) {
        throw PasswdError(PasswdError::LINE_INVALID, "howdy");
    }

    return u;   // never reached. in this fake-function, we only
                // demonstrate how to cause an error.
}

// how to call code that might throw an error of type PasswdError
TEST(passwd_error_suite, throwing_and_catching)
{
    try {
        parse_passwd_line__fake("some:line:with:an:error");
    }
    catch (const PasswdError& error) {           // <--- catch-block entered if a PasswdError object is thrown
        PasswdError::Code code = error.code;
        std::string msg = error.msg;

        ASSERT_EQ(code, PasswdError::LINE_INVALID);
        ASSERT_EQ(msg, "howdy");
    }
}

std::string Usage

Dissecting lines like

jfasch:x:1000:1000:Joerg Faschingbauer:/home/jfasch:/bin/bash

will likely require the following tools.

  • Searching for the field delimiter ':'. See std::string::find(). Take care of not-found conditions; s.npos is typically returned by string searches.

  • Given start and end positions of a fields, you need to take a substring. See std::string::substr().

  • Numeric fields (UID and GID) have to be converted from strings. See std::stoi(). Don’t forget to handle error conditions (see the example in the corresponding section in std::string)

Dependencies

cluster_cxx03 C++ cluster_cxx03_exceptions Exceptions cluster_cxx03_data_encapsulation Data Encapsulation cluster_cxx03_standard_library_miscellanea The Standard Library: Miscellaneous Topics cluster_cxx03_exercises_misc Exercises: Miscellaneous cluster_c The C Programming Language cluster_c_introduction Introduction cxx03_introduction Introduction c_introduction_installation Installation cxx03_introduction->c_introduction_installation cxx03_exceptions_try_catch try - catch cxx03_exceptions_basics Basics cxx03_exceptions_try_catch->cxx03_exceptions_basics cxx03_data_encapsulation_classes_objects Classes and Objects cxx03_exceptions_try_catch->cxx03_data_encapsulation_classes_objects cxx03_data_encapsulation_classes_objects->cxx03_introduction cxx03_data_encapsulation_c Object Oriented Programming In Good Ol’ C cxx03_data_encapsulation_classes_objects->cxx03_data_encapsulation_c cxx03_data_encapsulation_c->cxx03_introduction cxx03_standard_library_miscellanea_string std::string cxx03_exercises_misc_passwd_parser Exercise: Parse A Line From /etc/passwd cxx03_exercises_misc_passwd_parser->cxx03_exceptions_try_catch cxx03_exercises_misc_passwd_parser->cxx03_standard_library_miscellanea_string

Footnotes