12.1. File Path Relative

  • Python works with both relative and absolute path

  • Path is relative to currently running script

  • Path separator \ (backslash) is used on Windows

  • Path separator / (slash) is used on *nix operating systems: Linux, macOS, BSD and other POSIX compliant OSes (excluding older versions of Windows)

  • In newer Windows versions both \ and / works the same

  • Relative paths works the same on Windows and *nix (Linux, macOS, BSD, etc.)

12.1.1. Current Directory

  • Path is relative to currently running script

  • . - Current directory

>>> FILE = 'myfile.txt'
>>> FILE = './myfile.txt'
>>> FILE = 'data/myfile.txt'
>>> FILE = './data/myfile.txt'

12.1.2. Upper Directory

  • Path is relative to currently running script

  • .. - Parent directory

>>> FILE = '../myfile.txt'
>>> FILE = '../data/myfile.txt'
>>> FILE = '../../myfile.txt'
>>> FILE = '../../data/myfile.txt'

12.1.3. Current Working Directory

  • Returns an absolute path to current working directory

>>> from pathlib import Path
>>>
>>>
>>> path = Path.cwd()
>>> print(path)  
/home/watney/

12.1.4. Good Practices

  • Never hardcode paths, use constant as a file name or file path

  • Convention (singular form): FILE, FILENAME, FILEPATH, PATH

  • Convention (plural form): FILES, FILENAMES, FILEPATHS, PATHS

  • Note, that PATH is usually used for other purposes (sys.path or os.getenv('PATH'))

>>> FILE = 'myfile.txt'
>>> FILES = [
...     'myfile.txt',
...     'myfile.csv',
... ]

12.1.5. Assignments

Code 12.9. Solution
"""
* Assignment: File Read Passwd
* Type: homework
* Complexity: hard
* Lines of code: 77 lines
* Time: 55 min

English:
    1. In the current directory, the following files were saved:
        a. `etc_passwd.txt` with content of `CONTENT_PASSWD` variable
        b. `etc_shadow.txt` with content of `CONTENT_SHADOW` variable
        c. `etc_group.txt` with content of `CONTENT_GROUP` variable
    2. Parse all files and collect in `list[dict]` format:
        a. `algorithm: str | None`
        b. `days_between_change: int | None`
        c. `disable_after: int | None`
        d. `disabled_on: int | None`
        e. `gecos: str`
        f. `gid: int`
        g. `groups: list[str]`
        h. `home: str`
        i. `last_changed: date | None`
        j. `locked: bool`
        k. `login: str`
        l. `notify_before: int | None`
        m. `password: str`
        n. `reserved: None`
        o. `salt: str | None`
        p. `shell: str`
        r. `uid: int`
        s. `validity: int | None`
    4. Define variable `result: list[dict]`
       with user data whose `UID` is greater than 1000
    5. Use `Path` from `pathlib`
    6. Run doctests - all must succeed

Polish:
    1. W obencym katalogu zapisano następujące pliki:
        a. `etc_passwd.txt` z zawartością zmiennej `CONTENT_PASSWD`
        b. `etc_shadow.txt` z zawartością zmiennej `CONTENT_SHADOW`
        c. `etc_group.txt` z zawartością zmiennej `CONTENT_GROUP`
    2. Sparsuj wszystkie pliki i zbierz dane w formacie `list[dict]`:
        a. `algorithm: str | None`
        b. `days_between_change: int | None`
        c. `disable_after: int | None`
        d. `disabled_on: int | None`
        e. `gecos: str`
        f. `gid: int`
        g. `groups: list[str]`
        h. `home: str`
        i. `last_changed: date | None`
        j. `locked: bool`
        k. `login: str`
        l. `notify_before: int | None`
        m. `password: str`
        n. `reserved: None`
        o. `salt: str | None`
        p. `shell: str`
        r. `uid: int`
        s. `validity: int | None`
    4. Zdefiniuj zmienną `result: list[dict]` z danymi użytkowników
       których `UID` jest większy niż 1000
    5. Użyj `Path` z `pathlib`
    6. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `date(1970, 1, 1)`
    * `timedelta(days=...)

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from pprint import pprint

    >>> pprint(result)
    [{'algorithm': 'SHA-512',
      'days_between_change': None,
      'disable_after': None,
      'disabled_on': None,
      'gecos': 'Mark Watney',
      'gid': 1000,
      'groups': ['user', 'staff'],
      'home': '/home/mwatney',
      'last_changed': datetime.date(2015, 4, 25),
      'locked': False,
      'login': 'mwatney',
      'notify_before': None,
      'password': 'bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.',
      'reserved': '',
      'salt': '5H0QpwprRiJQR19Y',
      'shell': '/bin/bash',
      'uid': 1000,
      'validity': None},
     {'algorithm': 'SHA-512',
      'days_between_change': 0,
      'disable_after': None,
      'disabled_on': None,
      'gecos': 'Melissa Lewis',
      'gid': 1001,
      'groups': ['user', 'staff', 'admin'],
      'home': '/home/mlewis',
      'last_changed': datetime.date(2015, 7, 16),
      'locked': False,
      'login': 'mlewis',
      'notify_before': 7,
      'password': 'tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50',
      'reserved': '',
      'salt': 'P9zn0KwR',
      'shell': '/bin/bash',
      'uid': 1001,
      'validity': 99999},
     {'algorithm': 'MD5',
      'days_between_change': 0,
      'disable_after': 30,
      'disabled_on': datetime.date(2005, 11, 9),
      'gecos': 'Rick Martinez',
      'gid': 1002,
      'groups': ['user', 'staff'],
      'home': '/home/rmartinez',
      'last_changed': datetime.date(2005, 2, 11),
      'locked': False,
      'login': 'rmartinez',
      'notify_before': 5,
      'password': 'SWlkjRWexrXYgc98F.',
      'reserved': '',
      'salt': '.QKDPc5E',
      'shell': '/bin/bash',
      'uid': 1002,
      'validity': 90},
     {'algorithm': None,
      'days_between_change': 0,
      'disable_after': None,
      'disabled_on': None,
      'gecos': 'Alex Vogel',
      'gid': 1003,
      'groups': ['user'],
      'home': '/home/avogel',
      'last_changed': datetime.date(2014, 12, 27),
      'locked': True,
      'login': 'avogel',
      'notify_before': 7,
      'password': None,
      'reserved': '',
      'salt': None,
      'shell': '/bin/bash',
      'uid': 1003,
      'validity': 99999},
     {'algorithm': None,
      'days_between_change': 0,
      'disable_after': None,
      'disabled_on': None,
      'gecos': 'Beth Johanssen',
      'gid': 1004,
      'groups': ['user', 'staff', 'admin'],
      'home': '/home/bjohanssen',
      'last_changed': datetime.date(2014, 12, 27),
      'locked': True,
      'login': 'bjohanssen',
      'notify_before': 7,
      'password': None,
      'reserved': '',
      'salt': None,
      'shell': '/bin/bash',
      'uid': 1004,
      'validity': 99999},
     {'algorithm': None,
      'days_between_change': 0,
      'disable_after': None,
      'disabled_on': None,
      'gecos': 'Chris Beck',
      'gid': 1005,
      'groups': ['user', 'staff'],
      'home': '/home/cbeck',
      'last_changed': datetime.date(2014, 12, 27),
      'locked': True,
      'login': 'cbeck',
      'notify_before': 7,
      'password': None,
      'reserved': '',
      'salt': None,
      'shell': '/bin/bash',
      'uid': 1005,
      'validity': 99999}]


      >>> from os import remove
      >>> remove(FILE_GROUP)
      >>> remove(FILE_SHADOW)
      >>> remove(FILE_PASSWD)
"""

from datetime import date, timedelta
from pathlib import Path


CONTENT_PASSWD = """##
# `/etc/passwd` structure:
#   - login: user's login name
#   - password: 'x' indicates that shadow passwords are used
#   - uid: user's ID number
#   - gid: user's group ID number
#   - gecos: user's full name (firstname and lastname)
#   - home: user's home directory
#   - shell: user's shell 
##

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
nobody:x:99:99:Nobody:/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
mwatney:x:1000:1000:Mark Watney:/home/mwatney:/bin/bash
mlewis:x:1001:1001:Melissa Lewis:/home/mlewis:/bin/bash
rmartinez:x:1002:1002:Rick Martinez:/home/rmartinez:/bin/bash
avogel:x:1003:1003:Alex Vogel:/home/avogel:/bin/bash
bjohanssen:x:1004:1004:Beth Johanssen:/home/bjohanssen:/bin/bash
cbeck:x:1005:1005:Chris Beck:/home/cbeck:/bin/bash
"""


CONTENT_SHADOW = """##
# `/etc/shadow` structure:
#   - login: user's login from `/etc/passwd`
#   - password: encrypted password (see below for more details)
#   - last_changed: when was the password last changed (days since 1970-01-01)
#   - days_between_change: minimum days between password changes (0 = changed at any time)
#   - validity: days after which the password must be changed (99999 = 273 years)
#   - notify_before: days to warn user of an expiring password (7 = week before expiration)
#   - disable_after: days after password expires and account is disabled
#   - disabled_on: time since account was disabled (days since 1970-01-01)
#   - reserved: reserved field for possible future use
#
# Password field (split by `$`):
#   - algorithm
#   - salt
#   - password hash
#
# Password algorithms:
#   - '1' - MD5
#   - '2a' - Blowfish
#   - '2y' - Blowfish
#   - '5' - SHA-256
#   - '6' - SHA-512
#
# Password special chars:
#   - ' ' - password is not required to log in
#   - '*' - account is disabled, cannot be unlocked, and no password has ever been set
#   - '!' - account is locked, can be unlocked, and no password has ever been set
#   - '!<password_hash>' - account is locked, can be unlocked, but password is set
#   - '!!' - account created, waiting for initial password to be set by admin
##

root:$6$Ke02nYgo.9v0SF4p$hjztYvo/M4buqO4oBX8KZTftjCn6fE4cV5o/I95QPekeQpITwFTRbDUBYBLIUx2mhorQoj9bLN8v.w6btE9xy1:16431:0:99999:7:::
adm:*:16431:0:99999:7:::
mwatney:$6$5H0QpwprRiJQR19Y$bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.:16550::::::
mlewis:$6$P9zn0KwR$tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50:16632:0:99999:7:::
rmartinez:$1$.QKDPc5E$SWlkjRWexrXYgc98F.:12825:0:90:5:30:13096:
avogel:!!:16431:0:99999:7:::
bjohanssen:!:16431:0:99999:7:::
cbeck:*:16431:0:99999:7:::
"""


CONTENT_GROUP = """##
# `/etc/group` structure:
#   - name: group name
#   - password: 'x' indicates that shadow passwords are used
#   - gid: group id
#   - members: comma-separated logins from `/etc/passwd`
##

root::0:root
other::1:
bin::2:root,bin,daemon
sys::3:root,bin,sys,adm
adm::4:root,adm,daemon
mail::6:root
daemon::12:root,daemon
sysadmin::14:root
user::1001:mwatney,mlewis,rmartinez,avogel,bjohanssen,cbeck
staff::1002:mwatney,mlewis,rmartinez,bjohanssen,cbeck
admin::1003:mlewis,bjohanssen
nobody::60001:
noaccess::60002:
nogroup::65534:
"""

# In the current directory the following files were saved:
# - `etc_passwd.txt` with content of `CONTENT_PASSWD` variable
# - `etc_shadow.txt` with content of `CONTENT_SHADOW` variable
# - `etc_group.txt` with content of `CONTENT_GROUP` variable
CURRENT_DIR = Path(__file__).parent

FILE_PASSWD = CURRENT_DIR / 'etc-passwd.txt'
FILE_SHADOW = CURRENT_DIR / 'etc-shadow.txt'
FILE_GROUP = CURRENT_DIR / 'etc-group.txt'

FILE_PASSWD.write_text(CONTENT_PASSWD)
FILE_SHADOW.write_text(CONTENT_SHADOW)
FILE_GROUP.write_text(CONTENT_GROUP)

SECOND = 1
MINUTE = 60 * SECOND
HOUR = 60 * MINUTE
DAY = 24 * HOUR

ALGORITHMS = {
    '1': 'MD5',
    '2a': 'Blowfish',
    '2y': 'Blowfish',
    '5': 'SHA-256',
    '6': 'SHA-512',
}


# Parse all files and collect in `list[dict]` format:
# - algorithm: str | None
# - days_between_change: int | None
# - disable_after: int | None
# - disabled_on: int | None
# - gecos: str
# - gid: int
# - groups: list[str]
# - home: str
# - last_changed: date | None
# - locked: bool
# - login: str
# - notify_before: int | None
# - password: str
# - reserved: None
# - salt: str | None
# - shell: str
# - uid: int
# - validity: int | None
# Define variable `result: list[dict]`
# with user data whose `UID` is greater than 1000
# type: list[dict]
result = ...