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: 112 lines
* Time: 55 min

English:
    1. Save listings content to files:
        a. `etc_passwd.txt`
        b. `etc_shadow.txt`
        c. `etc_group.txt`
    2. Copy also comments and empty lines
    3. Parse files and convert it to `result: list[dict]`
    4. Return list of users with `UID` greater than 1000
    5. User dict should contains data collected from all files
    6. Run doctests - all must succeed

Polish:
    1. Zapisz treści listingów do plików:
        a. `etc_passwd.txt`
        b. `etc_shadow.txt`
        c. `etc_group.txt`
    2. Skopiuj również komentarze i puste linie
    3. Sparsuj plik i przedstaw go w formacie `result: list[dict]`
    4. Zwróć listę użytkowników, których `UID` jest większy niż 1000
    5. Dict użytkownika powinien zawierać dane z wszystkich plików
    6. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `from datetime import date`
    * `date.fromtimestamp(timestamp: int)`

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

    >>> pprint(result)
    [{'algorithm': None,
      'gid': 1000,
      'groups': ['user', 'staff'],
      'home': '/home/mwatney',
      'last_changed': datetime.date(2015, 4, 25),
      'locked': True,
      'password': None,
      'shell': '/bin/bash',
      'uid': 1000,
      'username': 'mwatney'},
     {'algorithm': 'SHA-512',
      'gid': 1001,
      'groups': ['user', 'staff', 'admin'],
      'home': '/home/mlewis',
      'last_changed': datetime.date(2015, 7, 16),
      'locked': False,
      'password': 'tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50',
      'shell': '/bin/bash',
      'uid': 1001,
      'username': 'mlewis'},
     {'algorithm': 'MD5',
      'gid': 1002,
      'groups': ['user', 'staff'],
      'home': '/home/rmartinez',
      'last_changed': datetime.date(2005, 2, 11),
      'locked': False,
      'password': 'SWlkjRWexrXYgc98F.',
      'shell': '/bin/bash',
      'uid': 1002,
      'username': 'rmartinez'},
     {'algorithm': None,
      'gid': 1003,
      'groups': ['user'],
      'home': '/home/avogel',
      'last_changed': datetime.date(2014, 12, 27),
      'locked': True,
      'password': None,
      'shell': '/bin/bash',
      'uid': 1003,
      'username': 'avogel'},
     {'algorithm': None,
      'gid': 1004,
      'groups': ['user', 'staff', 'admin'],
      'home': '/home/bjohanssen',
      'last_changed': datetime.date(2014, 12, 27),
      'locked': True,
      'password': None,
      'shell': '/bin/bash',
      'uid': 1004,
      'username': 'bjohanssen'},
     {'algorithm': None,
      'gid': 1005,
      'groups': ['user', 'staff'],
      'home': '/home/cbeck',
      'last_changed': datetime.date(2014, 12, 27),
      'locked': True,
      'password': None,
      'shell': '/bin/bash',
      'uid': 1005,
      'username': 'cbeck'}]

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

from datetime import date
from os.path import dirname, join


BASE_DIR = dirname(__file__)
FILE_GROUP = join(BASE_DIR, 'etc-group.txt')
FILE_SHADOW = join(BASE_DIR, 'etc-shadow.txt')
FILE_PASSWD = join(BASE_DIR, 'etc-passwd.txt')


CONTENT_GROUP = """##
# `/etc/group` structure
#   - groupname: group name
#   - password: to access group, `x` indicates that shadow passwords are used)
#   - gid: group id
#   - members: comma-separated usernames 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:"""


CONTENT_PASSWD = """##
# `/etc/passwd` structure:
#   - username: 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
#   - username: user's username from `/etc/passwd`
#   - password: user's encrypted password (look down for more details)
#   - last_changed: last password change, days since 1970-01-01
#   - days_between_change: minimum days between password changes, 0 - changed at any time
#   - validity: password validity, days after which password must be changed, 99999 - many, many years
#   - notify_before: warning threshold, days to warn user of an expiring password, 7 - full week
#   - disable_after: account inactive, days after password expires and account is disabled
#   - disabled_on: time since account is 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:
#   - ` ` (blank entry) - password is not required to log in
#   - `*` (asterisk) - account is disabled, cannot be unlocked, no password has ever been set
#   - `!` (exclamation mark) - account is locked, can be unlocked, no password has ever been set
#   - `!<password_hash>` - account is locked, can be unlocked, but password is set
#   - `!!` (two exclamation marks) - account created, waiting for initial password to be set by admin
##

root:$6$Ke02nYgo.9v0SF4p$hjztYvo/M4buqO4oBX8KZTftjCn6fE4cV5o/I95QPekeQpITwFTRbDUBYBLIUx2mhorQoj9bLN8v.w6btE9xy1:16431:0:99999:7:::
adm:$6$5H0QpwprRiJQR19Y$bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.:16431:0:99999:7:::
mwatney:!!: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:::
"""

with open(FILE_GROUP, mode='w') as file:
    file.write(CONTENT_GROUP)

with open(FILE_PASSWD, mode='w') as file:
    file.write(CONTENT_PASSWD)

with open(FILE_SHADOW, mode='w') as file:
    file.write(CONTENT_SHADOW)

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

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

# Joined data from all files for users with `UID` greater than 1000
# type: list[dict]
result = ...