The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
NAME
    Unix::Passwd::File - Manipulate /etc/{passwd,shadow,group,gshadow}
    entries

VERSION
    version 0.11

SYNOPSIS
     use Unix::Passwd::File;

     # list users. by default uses files in /etc (/etc/passwd, /etc/shadow, et al)
     my $res = list_users(); # [200, "OK", ["root", ...]]

     # change location of files, return details
     $res = list_users(etc_dir=>"/some/path", detail=>1);
         # [200, "OK", [{user=>"root", uid=>0, ...}, ...]]

     # also return detail, but return array entries instead of hash
     $res = list_users(detail=>1, with_field_names=>0);
         # [200, "OK", [["root", "x", 0, ...], ...]]

     # get user/group information
     $res = get_group(user=>"buzz"); # [200, "OK", {user=>"buzz", uid=>501, ...}]
     $res = get_user(user=>"neil");  # [404, "Not found"]

     # check whether user/group exists
     say user_exists(user=>"buzz");   # 1
     say group_exists(group=>"neil"); # 0

     # get all groups that user is member of
     $res = get_user_groups(user=>"buzz"); # [200, "OK", ["buzz", "nasa"]]

     # check whether user is member of a group
     $res = is_member(user=>"buzz", group=>"nasa"); # 1

     # adding user/group, by default adding user will also add a group with the same
     # name
     $res = add_user (user =>"steven", ...); # [200, "OK", {uid=>540, gid=>541}]
     $res = add_group(group=>"steven", ...); # [412, "Group already exists"]

     # modify user/group
     $res = modify_user(user=>"steven", home=>"/newhome/steven"); # [200, "OK"]
     $res = modify_group(group=>"neil"); # [404, "Not found"]

     # deleting user will also delete user's group
     $res = delete_user(user=>"neil");

     # change user password
     $res = set_user_password(user=>"steven", pass=>"foobar");
     $res = modify_user(user=>"steven", pass=>"foobar"); # same thing

     # add/delete user to/from group
     $res = add_user_to_group(user=>"steven", group=>"wheel");
     $res = delete_user_from_group(user=>"steven", group=>"wheel");

     # others
     $res = get_max_uid(); # [200, "OK", 65535]
     $res = get_max_gid(); # [200, "OK", 65534]

DESCRIPTION
    This module can be used to read and manipulate entries in Unix system
    password files (/etc/passwd, /etc/group, /etc/group, /etc/gshadow; but
    can also be told to search in custom location, for testing purposes).

    This module uses a procedural (non-OO) interface. Each function in this
    module open and read the passwd files once. Read-only functions like
    `list_users()` and `get_max_gid()` open in read-only mode. Functions
    that might write to the files like `add_user()` or `delete_group()`
    first lock `passwd.lock` file, open in read+write mode and also read the
    files in the first pass, then seek to the beginning and write back the
    files.

    No caching is done so you should do your own if you need to.

FUNCTIONS
  add_delete_user_groups(%args) -> [status, msg, result, meta]
    Add or delete user from one or several groups.

    This can be used to reduce several "add_user_to_group()" and/or
    "delete_user_from_group()" calls to a single call. So:

        add_delete_user_groups(user=>'u',add_to=>['a','b'],delete_from=>['c','d']);

    is equivalent to:

        add_user_to_group     (user=>'u', group=>'a');
        add_user_to_group     (user=>'u', group=>'b');
        delete_user_from_group(user=>'u', group=>'c');
        delete_user_from_group(user=>'u', group=>'d');

    except that "add_delete_user_groups()" does it in one pass.

    Arguments ('*' denotes required arguments):

    *   add_to => *array* (default: [])

        List of group names to add the user as member of.

    *   delete_from => *array* (default: [])

        List of group names to remove the user as member of.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   user* => *any*

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  add_group(%args) -> [status, msg, result, meta]
    Add a new group.

    Arguments ('*' denotes required arguments):

    *   backup => *bool* (default: 0)

        Whether to backup when modifying files.

        Backup is written with ".bak" extension in the same directory.
        Unmodified file will not be backed up. Previous backup will be
        overwritten.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   gid => *any*

        Pick a specific new GID.

        Adding a new group with duplicate GID is allowed.

    *   group* => *any*

    *   max_gid => *int* (default: 65535)

        Pick a range for new GID.

        If a free GID between "min_gid" and "max_gid" is not found, error
        412 is returned.

    *   members => *any*

        Fill initial members.

    *   min_gid => *int* (default: 1000)

        Pick a range for new GID.

        If a free GID between "min_gid" and "max_gid" is not found, error
        412 is returned.

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  add_user(%args) -> [status, msg, result, meta]
    Add a new user.

    Arguments ('*' denotes required arguments):

    *   backup => *bool* (default: 0)

        Whether to backup when modifying files.

        Backup is written with ".bak" extension in the same directory.
        Unmodified file will not be backed up. Previous backup will be
        overwritten.

    *   encpass => *str*

        Encrypted password.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   expire_date => *int*

        The date of expiration of the account, expressed as the number of
        days since Jan 1, 1970.

    *   gecos => *str*

        Usually, it contains the full username.

    *   gid => *any*

        Pick a specific GID when creating group.

        Duplicate GID is allowed.

    *   group => *any*

        Select primary group (default is group with same name as user).

        Normally, a user's primary group with group with the same name as
        user, which will be created if does not already exist. You can pick
        another group here, which must already exist (and in this case, the
        group with the same name as user will not be created).

    *   home => *str*

        User's home directory.

    *   last_pwchange => *int*

        The date of the last password change, expressed as the number of
        days since Jan 1, 1970.

    *   max_gid => *any*

        Pick a range for GID when creating group.

    *   max_pass_age => *int*

        The number of days after which the user will have to change her
        password.

    *   max_uid => *int* (default: 65535)

        Pick a range for new UID.

        If a free UID between "min_uid" and "max_uid" is not found, error
        412 is returned.

    *   min_gid => *any*

        Pick a range for GID when creating group.

    *   min_pass_age => *int*

        The number of days the user will have to wait before she will be
        allowed to change her password again.

    *   min_uid => *int* (default: 1000)

        Pick a range for new UID.

        If a free UID between "min_uid" and "max_uid" is not found, error
        412 is returned.

    *   pass => *str*

        Password, generally should be "x" which means password is encrypted
        in shadow.

    *   pass_inactive_period => *int*

        The number of days after a password has expired (see max_pass_age)
        during which the password should still be accepted (and user should
        update her password during the next login).

    *   pass_warn_period => *int*

        The number of days before a password is going to expire (see
        max_pass_age) during which the user should be warned.

    *   shell => *str*

        User's home directory.

    *   uid => *any*

        Pick a specific new UID.

        Adding a new user with duplicate UID is allowed.

    *   user* => *any*

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  add_user_to_group(%args) -> [status, msg, result, meta]
    Add user to a group.

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   group* => *any*

    *   user* => *any*

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  delete_group(%args) -> [status, msg, result, meta]
    Delete a group.

    Arguments ('*' denotes required arguments):

    *   backup => *bool* (default: 0)

        Whether to backup when modifying files.

        Backup is written with ".bak" extension in the same directory.
        Unmodified file will not be backed up. Previous backup will be
        overwritten.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   group* => *any*

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  delete_user(%args) -> [status, msg, result, meta]
    Delete a user.

    Arguments ('*' denotes required arguments):

    *   backup => *bool* (default: 0)

        Whether to backup when modifying files.

        Backup is written with ".bak" extension in the same directory.
        Unmodified file will not be backed up. Previous backup will be
        overwritten.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   user* => *any*

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  delete_user_from_group(%args) -> [status, msg, result, meta]
    Delete user from a group.

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   group* => *any*

    *   user* => *any*

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  get_group(%args) -> [status, msg, result, meta]
    Get group details by group name or gid.

    Either "group" OR "gid" must be specified.

    The function is not dissimilar to Unix's "getgrnam()" or "getgrgid()".

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   gid => *int*

    *   group => *str*

    *   with_field_names => *bool* (default: 1)

        If false, don't return hash.

        By default, a hashref is returned containing field names and its
        values, e.g. "{group=""neil", pass=>"x", gid=>500, ...}>. With
        "with_field_names="0>, an arrayref is returned instead: "["neil",
        "x", 500, ...]".

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  get_max_gid(%args) -> [status, msg, result, meta]
    Get maximum GID used.

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  get_max_uid(%args) -> [status, msg, result, meta]
    Get maximum UID used.

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  get_user(%args) -> [status, msg, result, meta]
    Get user details by username or uid.

    Either "user" OR "uid" must be specified.

    The function is not dissimilar to Unix's "getpwnam()" or "getpwuid()".

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   uid => *int*

    *   user => *str*

    *   with_field_names => *bool* (default: 1)

        If false, don't return hash.

        By default, a hashref is returned containing field names and its
        values, e.g. "{user=""neil", pass=>"x", uid=>500, ...}>. With
        "with_field_names="0>, an arrayref is returned instead: "["neil",
        "x", 500, ...]".

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  get_user_groups(%args) -> [status, msg, result, meta]
    Return groups which the user belongs to.

    Arguments ('*' denotes required arguments):

    *   detail => *bool* (default: 0)

        If true, return all fields instead of just group names.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   user* => *any*

    *   with_field_names => *bool* (default: 1)

        If false, don't return hash for each entry.

        By default, when "detail="1>, a hashref is returned for each entry
        containing field names and its values, e.g. "{group=""neil",
        pass=>"x", gid=>500, ...}>. With "with_field_names="0>, an arrayref
        is returned instead: "["neil", "x", 500, ...]".

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  group_exists(%args) -> bool
    Check whether group exists.

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   gid => *int*

    *   group => *str*

    Return value:

  is_member(%args) -> bool
    Check whether user is member of a group.

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   group* => *any*

    *   user* => *any*

    Return value:

  list_groups(%args) -> [status, msg, result, meta]
    List Unix groups in group file.

    Arguments ('*' denotes required arguments):

    *   detail => *bool* (default: 0)

        If true, return all fields instead of just group names.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   with_field_names => *bool* (default: 1)

        If false, don't return hash for each entry.

        By default, when "detail="1>, a hashref is returned for each entry
        containing field names and its values, e.g. "{group=""neil",
        pass=>"x", gid=>500, ...}>. With "with_field_names="0>, an arrayref
        is returned instead: "["neil", "x", 500, ...]".

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  list_users(%args) -> [status, msg, result, meta]
    List Unix users in passwd file.

    Arguments ('*' denotes required arguments):

    *   detail => *bool* (default: 0)

        If true, return all fields instead of just usernames.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   with_field_names => *bool* (default: 1)

        If false, don't return hash for each entry.

        By default, when "detail="1>, a hashref is returned for each entry
        containing field names and its values, e.g. "{user=""neil",
        pass=>"x", uid=>500, ...}>. With "with_field_names="0>, an arrayref
        is returned instead: "["neil", "x", 500, ...]".

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  list_users_and_groups(%args) -> [status, msg, result, meta]
    List Unix users and groups in passwd/group files.

    This is basically "list_users()" and "list_groups()" combined, so you
    can get both data in a single call. Data is returned in an array. Users
    list is in the first element, groups list in the second.

    Arguments ('*' denotes required arguments):

    *   detail => *bool* (default: 0)

        If true, return all fields instead of just names.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   with_field_names => *bool* (default: 1)

        If false, don't return hash for each entry.

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  modify_group(%args) -> [status, msg, result, meta]
    Modify an existing group.

    Specify arguments to modify corresponding fields. Unspecified fields
    will not be modified.

    Arguments ('*' denotes required arguments):

    *   admins => *str*

        It must be a comma-separated list of user names, or empty.

    *   backup => *bool* (default: 0)

        Whether to backup when modifying files.

        Backup is written with ".bak" extension in the same directory.
        Unmodified file will not be backed up. Previous backup will be
        overwritten.

    *   encpass => *str*

        Encrypted password.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   gid => *int*

        Numeric group ID.

    *   group => *str*

        Group name.

    *   members => *str*

        List of usernames that are members of this group, separated by
        commas.

    *   pass => *str*

        Password, generally should be "x" which means password is encrypted
        in gshadow.

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  modify_user(%args) -> [status, msg, result, meta]
    Modify an existing user.

    Specify arguments to modify corresponding fields. Unspecified fields
    will not be modified.

    Arguments ('*' denotes required arguments):

    *   backup => *bool* (default: 0)

        Whether to backup when modifying files.

        Backup is written with ".bak" extension in the same directory.
        Unmodified file will not be backed up. Previous backup will be
        overwritten.

    *   encpass => *str*

        Encrypted password.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   expire_date => *int*

        The date of expiration of the account, expressed as the number of
        days since Jan 1, 1970.

    *   gecos => *str*

        Usually, it contains the full username.

    *   gid => *int*

        Numeric primary group ID for this user.

    *   home => *str*

        User's home directory.

    *   last_pwchange => *int*

        The date of the last password change, expressed as the number of
        days since Jan 1, 1970.

    *   max_pass_age => *int*

        The number of days after which the user will have to change her
        password.

    *   min_pass_age => *int*

        The number of days the user will have to wait before she will be
        allowed to change her password again.

    *   pass => *str*

        Password, generally should be "x" which means password is encrypted
        in shadow.

    *   pass_inactive_period => *int*

        The number of days after a password has expired (see max_pass_age)
        during which the password should still be accepted (and user should
        update her password during the next login).

    *   pass_warn_period => *int*

        The number of days before a password is going to expire (see
        max_pass_age) during which the user should be warned.

    *   shell => *str*

        User's home directory.

    *   uid => *int*

        Numeric user ID.

    *   user => *str*

        User (login) name.

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  set_user_groups(%args) -> [status, msg, result, meta]
    Set the groups that a user is member of.

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   groups* => *array* (default: [])

        List of group names that user is member of.

        Aside from this list, user will not belong to any other group.

    *   user* => *any*

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  set_user_password(%args) -> [status, msg, result, meta]
    Set user's password.

    Arguments ('*' denotes required arguments):

    *   backup => *bool* (default: 0)

        Whether to backup when modifying files.

        Backup is written with ".bak" extension in the same directory.
        Unmodified file will not be backed up. Previous backup will be
        overwritten.

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   pass* => *any*

    *   user* => *any*

    Return value:

    Returns an enveloped result (an array).

    First element (status) is an integer containing HTTP status code (200
    means OK, 4xx caller error, 5xx function error). Second element (msg) is
    a string containing error message, or 'OK' if status is 200. Third
    element (result) is optional, the actual result. Fourth element (meta)
    is called result metadata and is optional, a hash that contains extra
    information.

  user_exists(%args) -> bool
    Check whether user exists.

    Arguments ('*' denotes required arguments):

    *   etc_dir => *str* (default: "/etc")

        Specify location of passwd files.

    *   uid => *int*

    *   user => *str*

    Return value:

SEE ALSO
    Old modules on CPAN which do not support shadow files are pretty useless
    to me (e.g. Unix::ConfigFile). Shadow passwords have been around since
    1988 (and in Linux since 1992), FFS!

    Passwd::Unix. I created a fork of Passwd::Unix v0.52 called
    Passwd::Unix::Alt in 2011 to fix some of the deficiencies/quirks in
    Passwd::Unix, including: lack of tests, insistence of running as root
    (despite allowing custom passwd files), use of not-so-ubiquitous bzip2,
    etc. Then in 2012 I decided to create Unix::Passwd::File. Here are how
    Unix::Passwd::File differs compared to Passwd::Unix (and
    Passwd::Unix::Alt):

    *   tests in distribution

    *   no need to run as root

    *   no need to be able to read the shadow file for some operations

        For example, "list_users()" will simply not return the "encpass"
        field if the shadow file is unreadable. Of course, access to shadow
        file is required when getting or setting password.

    *   strictly procedural (non-OO) interface

        I consider this a feature :-)

    *   detailed error message for each operation

    *   removal of global error variable

    *   working locking

        Locking is done by locking "passwd.lock" file.

    Setup::Unix::User and Setup::Unix::Group, which use this module.

    Rinci

HOMEPAGE
    Please visit the project's homepage at
    <https://metacpan.org/release/Unix-Passwd-File>.

SOURCE
    Source repository is at
    <https://github.com/sharyanto/perl-Unix-Passwd-File>.

BUGS
    Please report any bugs or feature requests on the bugtracker website
    <https://rt.cpan.org/Public/Dist/Display.html?Name=Unix-Passwd-File>

    When submitting a bug or request, please include a test-file or a patch
    to an existing test-file that illustrates the bug or desired feature.

AUTHOR
    Steven Haryanto <stevenharyanto@gmail.com>

COPYRIGHT AND LICENSE
    This software is copyright (c) 2014 by Steven Haryanto.

    This is free software; you can redistribute it and/or modify it under
    the same terms as the Perl 5 programming language system itself.