#!/usr/bin/python3

import os
import shutil
import pathlib
import json
import argparse
import logging

# Helper functions
def prompt_yes_no(msg, automatic_yes=False, ask_twice=False):

    assert type(automatic_yes) is bool
    assert type(ask_twice) is bool

    if automatic_yes:
        return True
    else: 
        YES_ANSWERS = ['y', 'Y']
        if ask_twice:
            return (input(msg + " [yY/nN]: ") in YES_ANSWERS and
                input("Are you sure? [yY/nN]: ") in YES_ANSWERS)
        else:
            return input(msg + " [yY/nN]: ") in YES_ANSWERS

def pathlink_link(original_dir, destination_dir, tracked_paths, execute_linking, automatic_yes):

    for tracked_path in tracked_paths:

        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)

        logging.info(f'--- processing path: {tracked_path}')

        if os.path.exists(destination_path):
            if not os.path.exists(original_path) and not os.path.islink(original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: corresponding link DOES NOT exist')
                if execute_linking and prompt_yes_no("Do you want to add a link?", automatic_yes=automatic_yes):
                    os.symlink(destination_path, original_path)
            elif os.path.islink(original_path):
                if os.readlink(original_path) == destination_path:
                    logging.info('destination_dir: path exists')
                    logging.info(f'original_dir: symbolic link already points to correct location')
                else:
                    logging.info('destination_dir: path exists')
                    logging.info(f'original_dir: symbolic link points to WRONG location: {os.readlink(original_path)}')
                    if execute_linking and prompt_yes_no("Do you want to overwrite it?"
                            , automatic_yes=automatic_yes, ask_twice=True):
                        os.unlink(original_path)
                        os.symlink(destination_path, original_path)
            elif os.path.isdir(original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: folder exists (NOT a symlink)')
                if execute_linking and prompt_yes_no("Do you want to overwrite it?"
                        , automatic_yes=automatic_yes, ask_twice=True):
                    shutil.rmtree(original_path)
                    os.symlink(destination_path, original_path)
            else: # should be a file
                logging.info('destination_dir: path exists')
                logging.info('original_dir: file exists (NOT a symlink)')
                if execute_linking and prompt_yes_no("Do you want to overwrite it?"
                        , automatic_yes=automatic_yes, ask_twice=True):
                    os.remove(original_path)
                    os.symlink(destination_path, original_path)
        else: # backup path doesn't exist
            if not os.path.exists(original_path) and not os.path.islink(original_path):
                logging.info("original_dir: nothing")
                logging.info("destination_dir: nothing")
            elif os.path.islink(original_path):
                logging.info("original_dir: symbolic link exists")
                logging.info("destination_dir: nothing")
            else: # original path is a file/dir
                logging.info('original_dir: file/folder exists')
                logging.info('destination_dir: nothing')
                if execute_linking and prompt_yes_no("Do you want to move file/folder to destination_dir and add link to original_dir?", automatic_yes=automatic_yes):
                    # Move original to backup location
                    backup_path_folder = os.path.abspath(os.path.join(destination_path, '..'))
                    pathlib.Path(backup_path_folder).mkdir(parents=True, exist_ok=True)
                    shutil.move(original_path, backup_path_folder)
                    # Create symlink
                    os.symlink(destination_path, original_path)

def pathlink_unlink(original_dir, destination_dir, tracked_paths, execute_linking, automatic_yes):

    for tracked_path in tracked_paths:

        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)

        logging.info(f'--- processing path: {tracked_path}')

        if (os.path.islink(original_path) 
            and os.readlink(original_path) == destination_path
            and os.path.exists(destination_path)):
            if execute_linking and prompt_yes_no("Do you want to remove the link?", automatic_yes=automatic_yes):
                os.unlink(original_path)
                shutil.move(destination_path, original_path)
        else:
            logging.info('Not a proper link. DIY')

def pathlink_single_folder(settings_id, settings_dict, mode, execute_linking, automatic_yes):

    logging.info(f'### processing settings_id: {settings_id}')

    try:
        tracked_paths = settings_dict['tracked_paths']
        original_dir = settings_dict['original_dir']
        destination_dir = settings_dict['destination_dir']
    except:
        raise Exception('error: settings json should have fields: tracked_paths, original_dir, destination_dir')

    assert type(tracked_paths) is list, "error: tracked_paths is not a list"
    # assert tracked_paths, "error: tracked_paths should not be empty"
    assert isinstance(original_dir, str) and os.path.isdir(original_dir), "error: original_dir is not a directory"
    assert isinstance(destination_dir, str) and os.path.isdir(destination_dir), "error: destination_dir is not a directory"
    assert mode == 'link' or mode == 'unlink', f'error: invalid mode: {mode}'

    if mode == 'link':
        pathlink_link(original_dir, destination_dir, tracked_paths, execute_linking, automatic_yes)
    elif mode == 'unlink':
        pathlink_unlink(original_dir, destination_dir, tracked_paths, execute_linking, automatic_yes)


def pathlink(settings_json_path, mode='link', settings_id='*', execute_linking=True, automatic_yes=False, silent=False):

    logging.basicConfig(level=(logging.WARNING if silent and automatic_yes else logging.INFO),
        format='%(message)s')

    logging.info(f'mode = {mode}, settings_id = {settings_id}')
    logging.info(f'settings_json_path = {settings_json_path}')

    assert type(settings_json_path) is str
    assert type(mode) is str
    assert type(settings_id) is str
    assert type(execute_linking) is bool
    assert type(automatic_yes) is bool

    assert os.path.isfile(settings_json_path), 'error: settings json should be a json file'
    with open(settings_json_path, 'r') as f:
        try:
            settings = json.load(f)
        except:
            raise Exception('error: settings json should be a json file')

    assert type(settings) is dict, 'error: settings json should be a dictionary'

    if settings_id == '*':
        assert settings, 'error: settings json should be a non-empty dictionary'
        for s_id, s_dict in settings.items():
            pathlink_single_folder(s_id, s_dict, mode, execute_linking, automatic_yes)
    else: # single id to link
        try:
            settings_dict = settings[settings_id]
        except:
            raise Exception('error: settings json should contain an entry for the chosen id')
        
        pathlink_single_folder(settings_id, settings_dict, mode, execute_linking, automatic_yes)


if __name__ == "__main__":

    # Parse script arguments
    parser = argparse.ArgumentParser()

    parser.add_argument('-m', '--mode', type=str, default='link', choices=['link', 'unlink'],
        help='chooses mode for execution: link (add symbolic links), unlink (remove symbolic links)')
    parser.add_argument('-d', '--dry-run', action='store_true',
        help='just print info without any action')
    parser.add_argument('--id', type=str, default='*',
        help='specifies which settings id from config to link (default: * (all ids))')
    # default settings json file
    SETTINGS_JSON_PATH = os.path.join(os.path.expanduser('~'), '.pathlinker.json')
    parser.add_argument('-s', '--settings-json', type=str, default=SETTINGS_JSON_PATH,
        help='specifies file with settings (default: ~/.pathlinker.json)')

    args = parser.parse_args()
    
    pathlink(args.settings_json, mode=args.mode, settings_id=args.id, execute_linking=not args.dry_run)


fd19fa57078d6cd49b025351cfabf66e80861829
<mask token>


def prompt_yes_no(msg, automatic_yes=False, ask_twice=False):
    assert type(automatic_yes) is bool
    assert type(ask_twice) is bool
    if automatic_yes:
        return True
    else:
        YES_ANSWERS = ['y', 'Y']
        if ask_twice:
            return input(msg + ' [yY/nN]: ') in YES_ANSWERS and input(
                'Are you sure? [yY/nN]: ') in YES_ANSWERS
        else:
            return input(msg + ' [yY/nN]: ') in YES_ANSWERS


def pathlink_link(original_dir, destination_dir, tracked_paths,
    execute_linking, automatic_yes):
    for tracked_path in tracked_paths:
        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)
        logging.info(f'--- processing path: {tracked_path}')
        if os.path.exists(destination_path):
            if not os.path.exists(original_path) and not os.path.islink(
                original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: corresponding link DOES NOT exist')
                if execute_linking and prompt_yes_no(
                    'Do you want to add a link?', automatic_yes=automatic_yes):
                    os.symlink(destination_path, original_path)
            elif os.path.islink(original_path):
                if os.readlink(original_path) == destination_path:
                    logging.info('destination_dir: path exists')
                    logging.info(
                        f'original_dir: symbolic link already points to correct location'
                        )
                else:
                    logging.info('destination_dir: path exists')
                    logging.info(
                        f'original_dir: symbolic link points to WRONG location: {os.readlink(original_path)}'
                        )
                    if execute_linking and prompt_yes_no(
                        'Do you want to overwrite it?', automatic_yes=
                        automatic_yes, ask_twice=True):
                        os.unlink(original_path)
                        os.symlink(destination_path, original_path)
            elif os.path.isdir(original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: folder exists (NOT a symlink)')
                if execute_linking and prompt_yes_no(
                    'Do you want to overwrite it?', automatic_yes=
                    automatic_yes, ask_twice=True):
                    shutil.rmtree(original_path)
                    os.symlink(destination_path, original_path)
            else:
                logging.info('destination_dir: path exists')
                logging.info('original_dir: file exists (NOT a symlink)')
                if execute_linking and prompt_yes_no(
                    'Do you want to overwrite it?', automatic_yes=
                    automatic_yes, ask_twice=True):
                    os.remove(original_path)
                    os.symlink(destination_path, original_path)
        elif not os.path.exists(original_path) and not os.path.islink(
            original_path):
            logging.info('original_dir: nothing')
            logging.info('destination_dir: nothing')
        elif os.path.islink(original_path):
            logging.info('original_dir: symbolic link exists')
            logging.info('destination_dir: nothing')
        else:
            logging.info('original_dir: file/folder exists')
            logging.info('destination_dir: nothing')
            if execute_linking and prompt_yes_no(
                'Do you want to move file/folder to destination_dir and add link to original_dir?'
                , automatic_yes=automatic_yes):
                backup_path_folder = os.path.abspath(os.path.join(
                    destination_path, '..'))
                pathlib.Path(backup_path_folder).mkdir(parents=True,
                    exist_ok=True)
                shutil.move(original_path, backup_path_folder)
                os.symlink(destination_path, original_path)


def pathlink_unlink(original_dir, destination_dir, tracked_paths,
    execute_linking, automatic_yes):
    for tracked_path in tracked_paths:
        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)
        logging.info(f'--- processing path: {tracked_path}')
        if os.path.islink(original_path) and os.readlink(original_path
            ) == destination_path and os.path.exists(destination_path):
            if execute_linking and prompt_yes_no(
                'Do you want to remove the link?', automatic_yes=automatic_yes
                ):
                os.unlink(original_path)
                shutil.move(destination_path, original_path)
        else:
            logging.info('Not a proper link. DIY')


<mask token>


def pathlink(settings_json_path, mode='link', settings_id='*',
    execute_linking=True, automatic_yes=False, silent=False):
    logging.basicConfig(level=logging.WARNING if silent and automatic_yes else
        logging.INFO, format='%(message)s')
    logging.info(f'mode = {mode}, settings_id = {settings_id}')
    logging.info(f'settings_json_path = {settings_json_path}')
    assert type(settings_json_path) is str
    assert type(mode) is str
    assert type(settings_id) is str
    assert type(execute_linking) is bool
    assert type(automatic_yes) is bool
    assert os.path.isfile(settings_json_path
        ), 'error: settings json should be a json file'
    with open(settings_json_path, 'r') as f:
        try:
            settings = json.load(f)
        except:
            raise Exception('error: settings json should be a json file')
    assert type(settings
        ) is dict, 'error: settings json should be a dictionary'
    if settings_id == '*':
        assert settings, 'error: settings json should be a non-empty dictionary'
        for s_id, s_dict in settings.items():
            pathlink_single_folder(s_id, s_dict, mode, execute_linking,
                automatic_yes)
    else:
        try:
            settings_dict = settings[settings_id]
        except:
            raise Exception(
                'error: settings json should contain an entry for the chosen id'
                )
        pathlink_single_folder(settings_id, settings_dict, mode,
            execute_linking, automatic_yes)


<mask token>

<mask token>


def prompt_yes_no(msg, automatic_yes=False, ask_twice=False):
    assert type(automatic_yes) is bool
    assert type(ask_twice) is bool
    if automatic_yes:
        return True
    else:
        YES_ANSWERS = ['y', 'Y']
        if ask_twice:
            return input(msg + ' [yY/nN]: ') in YES_ANSWERS and input(
                'Are you sure? [yY/nN]: ') in YES_ANSWERS
        else:
            return input(msg + ' [yY/nN]: ') in YES_ANSWERS


def pathlink_link(original_dir, destination_dir, tracked_paths,
    execute_linking, automatic_yes):
    for tracked_path in tracked_paths:
        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)
        logging.info(f'--- processing path: {tracked_path}')
        if os.path.exists(destination_path):
            if not os.path.exists(original_path) and not os.path.islink(
                original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: corresponding link DOES NOT exist')
                if execute_linking and prompt_yes_no(
                    'Do you want to add a link?', automatic_yes=automatic_yes):
                    os.symlink(destination_path, original_path)
            elif os.path.islink(original_path):
                if os.readlink(original_path) == destination_path:
                    logging.info('destination_dir: path exists')
                    logging.info(
                        f'original_dir: symbolic link already points to correct location'
                        )
                else:
                    logging.info('destination_dir: path exists')
                    logging.info(
                        f'original_dir: symbolic link points to WRONG location: {os.readlink(original_path)}'
                        )
                    if execute_linking and prompt_yes_no(
                        'Do you want to overwrite it?', automatic_yes=
                        automatic_yes, ask_twice=True):
                        os.unlink(original_path)
                        os.symlink(destination_path, original_path)
            elif os.path.isdir(original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: folder exists (NOT a symlink)')
                if execute_linking and prompt_yes_no(
                    'Do you want to overwrite it?', automatic_yes=
                    automatic_yes, ask_twice=True):
                    shutil.rmtree(original_path)
                    os.symlink(destination_path, original_path)
            else:
                logging.info('destination_dir: path exists')
                logging.info('original_dir: file exists (NOT a symlink)')
                if execute_linking and prompt_yes_no(
                    'Do you want to overwrite it?', automatic_yes=
                    automatic_yes, ask_twice=True):
                    os.remove(original_path)
                    os.symlink(destination_path, original_path)
        elif not os.path.exists(original_path) and not os.path.islink(
            original_path):
            logging.info('original_dir: nothing')
            logging.info('destination_dir: nothing')
        elif os.path.islink(original_path):
            logging.info('original_dir: symbolic link exists')
            logging.info('destination_dir: nothing')
        else:
            logging.info('original_dir: file/folder exists')
            logging.info('destination_dir: nothing')
            if execute_linking and prompt_yes_no(
                'Do you want to move file/folder to destination_dir and add link to original_dir?'
                , automatic_yes=automatic_yes):
                backup_path_folder = os.path.abspath(os.path.join(
                    destination_path, '..'))
                pathlib.Path(backup_path_folder).mkdir(parents=True,
                    exist_ok=True)
                shutil.move(original_path, backup_path_folder)
                os.symlink(destination_path, original_path)


def pathlink_unlink(original_dir, destination_dir, tracked_paths,
    execute_linking, automatic_yes):
    for tracked_path in tracked_paths:
        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)
        logging.info(f'--- processing path: {tracked_path}')
        if os.path.islink(original_path) and os.readlink(original_path
            ) == destination_path and os.path.exists(destination_path):
            if execute_linking and prompt_yes_no(
                'Do you want to remove the link?', automatic_yes=automatic_yes
                ):
                os.unlink(original_path)
                shutil.move(destination_path, original_path)
        else:
            logging.info('Not a proper link. DIY')


def pathlink_single_folder(settings_id, settings_dict, mode,
    execute_linking, automatic_yes):
    logging.info(f'### processing settings_id: {settings_id}')
    try:
        tracked_paths = settings_dict['tracked_paths']
        original_dir = settings_dict['original_dir']
        destination_dir = settings_dict['destination_dir']
    except:
        raise Exception(
            'error: settings json should have fields: tracked_paths, original_dir, destination_dir'
            )
    assert type(tracked_paths) is list, 'error: tracked_paths is not a list'
    assert isinstance(original_dir, str) and os.path.isdir(original_dir
        ), 'error: original_dir is not a directory'
    assert isinstance(destination_dir, str) and os.path.isdir(destination_dir
        ), 'error: destination_dir is not a directory'
    assert mode == 'link' or mode == 'unlink', f'error: invalid mode: {mode}'
    if mode == 'link':
        pathlink_link(original_dir, destination_dir, tracked_paths,
            execute_linking, automatic_yes)
    elif mode == 'unlink':
        pathlink_unlink(original_dir, destination_dir, tracked_paths,
            execute_linking, automatic_yes)


def pathlink(settings_json_path, mode='link', settings_id='*',
    execute_linking=True, automatic_yes=False, silent=False):
    logging.basicConfig(level=logging.WARNING if silent and automatic_yes else
        logging.INFO, format='%(message)s')
    logging.info(f'mode = {mode}, settings_id = {settings_id}')
    logging.info(f'settings_json_path = {settings_json_path}')
    assert type(settings_json_path) is str
    assert type(mode) is str
    assert type(settings_id) is str
    assert type(execute_linking) is bool
    assert type(automatic_yes) is bool
    assert os.path.isfile(settings_json_path
        ), 'error: settings json should be a json file'
    with open(settings_json_path, 'r') as f:
        try:
            settings = json.load(f)
        except:
            raise Exception('error: settings json should be a json file')
    assert type(settings
        ) is dict, 'error: settings json should be a dictionary'
    if settings_id == '*':
        assert settings, 'error: settings json should be a non-empty dictionary'
        for s_id, s_dict in settings.items():
            pathlink_single_folder(s_id, s_dict, mode, execute_linking,
                automatic_yes)
    else:
        try:
            settings_dict = settings[settings_id]
        except:
            raise Exception(
                'error: settings json should contain an entry for the chosen id'
                )
        pathlink_single_folder(settings_id, settings_dict, mode,
            execute_linking, automatic_yes)


<mask token>

<mask token>


def prompt_yes_no(msg, automatic_yes=False, ask_twice=False):
    assert type(automatic_yes) is bool
    assert type(ask_twice) is bool
    if automatic_yes:
        return True
    else:
        YES_ANSWERS = ['y', 'Y']
        if ask_twice:
            return input(msg + ' [yY/nN]: ') in YES_ANSWERS and input(
                'Are you sure? [yY/nN]: ') in YES_ANSWERS
        else:
            return input(msg + ' [yY/nN]: ') in YES_ANSWERS


def pathlink_link(original_dir, destination_dir, tracked_paths,
    execute_linking, automatic_yes):
    for tracked_path in tracked_paths:
        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)
        logging.info(f'--- processing path: {tracked_path}')
        if os.path.exists(destination_path):
            if not os.path.exists(original_path) and not os.path.islink(
                original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: corresponding link DOES NOT exist')
                if execute_linking and prompt_yes_no(
                    'Do you want to add a link?', automatic_yes=automatic_yes):
                    os.symlink(destination_path, original_path)
            elif os.path.islink(original_path):
                if os.readlink(original_path) == destination_path:
                    logging.info('destination_dir: path exists')
                    logging.info(
                        f'original_dir: symbolic link already points to correct location'
                        )
                else:
                    logging.info('destination_dir: path exists')
                    logging.info(
                        f'original_dir: symbolic link points to WRONG location: {os.readlink(original_path)}'
                        )
                    if execute_linking and prompt_yes_no(
                        'Do you want to overwrite it?', automatic_yes=
                        automatic_yes, ask_twice=True):
                        os.unlink(original_path)
                        os.symlink(destination_path, original_path)
            elif os.path.isdir(original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: folder exists (NOT a symlink)')
                if execute_linking and prompt_yes_no(
                    'Do you want to overwrite it?', automatic_yes=
                    automatic_yes, ask_twice=True):
                    shutil.rmtree(original_path)
                    os.symlink(destination_path, original_path)
            else:
                logging.info('destination_dir: path exists')
                logging.info('original_dir: file exists (NOT a symlink)')
                if execute_linking and prompt_yes_no(
                    'Do you want to overwrite it?', automatic_yes=
                    automatic_yes, ask_twice=True):
                    os.remove(original_path)
                    os.symlink(destination_path, original_path)
        elif not os.path.exists(original_path) and not os.path.islink(
            original_path):
            logging.info('original_dir: nothing')
            logging.info('destination_dir: nothing')
        elif os.path.islink(original_path):
            logging.info('original_dir: symbolic link exists')
            logging.info('destination_dir: nothing')
        else:
            logging.info('original_dir: file/folder exists')
            logging.info('destination_dir: nothing')
            if execute_linking and prompt_yes_no(
                'Do you want to move file/folder to destination_dir and add link to original_dir?'
                , automatic_yes=automatic_yes):
                backup_path_folder = os.path.abspath(os.path.join(
                    destination_path, '..'))
                pathlib.Path(backup_path_folder).mkdir(parents=True,
                    exist_ok=True)
                shutil.move(original_path, backup_path_folder)
                os.symlink(destination_path, original_path)


def pathlink_unlink(original_dir, destination_dir, tracked_paths,
    execute_linking, automatic_yes):
    for tracked_path in tracked_paths:
        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)
        logging.info(f'--- processing path: {tracked_path}')
        if os.path.islink(original_path) and os.readlink(original_path
            ) == destination_path and os.path.exists(destination_path):
            if execute_linking and prompt_yes_no(
                'Do you want to remove the link?', automatic_yes=automatic_yes
                ):
                os.unlink(original_path)
                shutil.move(destination_path, original_path)
        else:
            logging.info('Not a proper link. DIY')


def pathlink_single_folder(settings_id, settings_dict, mode,
    execute_linking, automatic_yes):
    logging.info(f'### processing settings_id: {settings_id}')
    try:
        tracked_paths = settings_dict['tracked_paths']
        original_dir = settings_dict['original_dir']
        destination_dir = settings_dict['destination_dir']
    except:
        raise Exception(
            'error: settings json should have fields: tracked_paths, original_dir, destination_dir'
            )
    assert type(tracked_paths) is list, 'error: tracked_paths is not a list'
    assert isinstance(original_dir, str) and os.path.isdir(original_dir
        ), 'error: original_dir is not a directory'
    assert isinstance(destination_dir, str) and os.path.isdir(destination_dir
        ), 'error: destination_dir is not a directory'
    assert mode == 'link' or mode == 'unlink', f'error: invalid mode: {mode}'
    if mode == 'link':
        pathlink_link(original_dir, destination_dir, tracked_paths,
            execute_linking, automatic_yes)
    elif mode == 'unlink':
        pathlink_unlink(original_dir, destination_dir, tracked_paths,
            execute_linking, automatic_yes)


def pathlink(settings_json_path, mode='link', settings_id='*',
    execute_linking=True, automatic_yes=False, silent=False):
    logging.basicConfig(level=logging.WARNING if silent and automatic_yes else
        logging.INFO, format='%(message)s')
    logging.info(f'mode = {mode}, settings_id = {settings_id}')
    logging.info(f'settings_json_path = {settings_json_path}')
    assert type(settings_json_path) is str
    assert type(mode) is str
    assert type(settings_id) is str
    assert type(execute_linking) is bool
    assert type(automatic_yes) is bool
    assert os.path.isfile(settings_json_path
        ), 'error: settings json should be a json file'
    with open(settings_json_path, 'r') as f:
        try:
            settings = json.load(f)
        except:
            raise Exception('error: settings json should be a json file')
    assert type(settings
        ) is dict, 'error: settings json should be a dictionary'
    if settings_id == '*':
        assert settings, 'error: settings json should be a non-empty dictionary'
        for s_id, s_dict in settings.items():
            pathlink_single_folder(s_id, s_dict, mode, execute_linking,
                automatic_yes)
    else:
        try:
            settings_dict = settings[settings_id]
        except:
            raise Exception(
                'error: settings json should contain an entry for the chosen id'
                )
        pathlink_single_folder(settings_id, settings_dict, mode,
            execute_linking, automatic_yes)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-m', '--mode', type=str, default='link', choices=[
        'link', 'unlink'], help=
        'chooses mode for execution: link (add symbolic links), unlink (remove symbolic links)'
        )
    parser.add_argument('-d', '--dry-run', action='store_true', help=
        'just print info without any action')
    parser.add_argument('--id', type=str, default='*', help=
        'specifies which settings id from config to link (default: * (all ids))'
        )
    SETTINGS_JSON_PATH = os.path.join(os.path.expanduser('~'),
        '.pathlinker.json')
    parser.add_argument('-s', '--settings-json', type=str, default=
        SETTINGS_JSON_PATH, help=
        'specifies file with settings (default: ~/.pathlinker.json)')
    args = parser.parse_args()
    pathlink(args.settings_json, mode=args.mode, settings_id=args.id,
        execute_linking=not args.dry_run)

import os
import shutil
import pathlib
import json
import argparse
import logging


def prompt_yes_no(msg, automatic_yes=False, ask_twice=False):
    assert type(automatic_yes) is bool
    assert type(ask_twice) is bool
    if automatic_yes:
        return True
    else:
        YES_ANSWERS = ['y', 'Y']
        if ask_twice:
            return input(msg + ' [yY/nN]: ') in YES_ANSWERS and input(
                'Are you sure? [yY/nN]: ') in YES_ANSWERS
        else:
            return input(msg + ' [yY/nN]: ') in YES_ANSWERS


def pathlink_link(original_dir, destination_dir, tracked_paths,
    execute_linking, automatic_yes):
    for tracked_path in tracked_paths:
        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)
        logging.info(f'--- processing path: {tracked_path}')
        if os.path.exists(destination_path):
            if not os.path.exists(original_path) and not os.path.islink(
                original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: corresponding link DOES NOT exist')
                if execute_linking and prompt_yes_no(
                    'Do you want to add a link?', automatic_yes=automatic_yes):
                    os.symlink(destination_path, original_path)
            elif os.path.islink(original_path):
                if os.readlink(original_path) == destination_path:
                    logging.info('destination_dir: path exists')
                    logging.info(
                        f'original_dir: symbolic link already points to correct location'
                        )
                else:
                    logging.info('destination_dir: path exists')
                    logging.info(
                        f'original_dir: symbolic link points to WRONG location: {os.readlink(original_path)}'
                        )
                    if execute_linking and prompt_yes_no(
                        'Do you want to overwrite it?', automatic_yes=
                        automatic_yes, ask_twice=True):
                        os.unlink(original_path)
                        os.symlink(destination_path, original_path)
            elif os.path.isdir(original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: folder exists (NOT a symlink)')
                if execute_linking and prompt_yes_no(
                    'Do you want to overwrite it?', automatic_yes=
                    automatic_yes, ask_twice=True):
                    shutil.rmtree(original_path)
                    os.symlink(destination_path, original_path)
            else:
                logging.info('destination_dir: path exists')
                logging.info('original_dir: file exists (NOT a symlink)')
                if execute_linking and prompt_yes_no(
                    'Do you want to overwrite it?', automatic_yes=
                    automatic_yes, ask_twice=True):
                    os.remove(original_path)
                    os.symlink(destination_path, original_path)
        elif not os.path.exists(original_path) and not os.path.islink(
            original_path):
            logging.info('original_dir: nothing')
            logging.info('destination_dir: nothing')
        elif os.path.islink(original_path):
            logging.info('original_dir: symbolic link exists')
            logging.info('destination_dir: nothing')
        else:
            logging.info('original_dir: file/folder exists')
            logging.info('destination_dir: nothing')
            if execute_linking and prompt_yes_no(
                'Do you want to move file/folder to destination_dir and add link to original_dir?'
                , automatic_yes=automatic_yes):
                backup_path_folder = os.path.abspath(os.path.join(
                    destination_path, '..'))
                pathlib.Path(backup_path_folder).mkdir(parents=True,
                    exist_ok=True)
                shutil.move(original_path, backup_path_folder)
                os.symlink(destination_path, original_path)


def pathlink_unlink(original_dir, destination_dir, tracked_paths,
    execute_linking, automatic_yes):
    for tracked_path in tracked_paths:
        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)
        logging.info(f'--- processing path: {tracked_path}')
        if os.path.islink(original_path) and os.readlink(original_path
            ) == destination_path and os.path.exists(destination_path):
            if execute_linking and prompt_yes_no(
                'Do you want to remove the link?', automatic_yes=automatic_yes
                ):
                os.unlink(original_path)
                shutil.move(destination_path, original_path)
        else:
            logging.info('Not a proper link. DIY')


def pathlink_single_folder(settings_id, settings_dict, mode,
    execute_linking, automatic_yes):
    logging.info(f'### processing settings_id: {settings_id}')
    try:
        tracked_paths = settings_dict['tracked_paths']
        original_dir = settings_dict['original_dir']
        destination_dir = settings_dict['destination_dir']
    except:
        raise Exception(
            'error: settings json should have fields: tracked_paths, original_dir, destination_dir'
            )
    assert type(tracked_paths) is list, 'error: tracked_paths is not a list'
    assert isinstance(original_dir, str) and os.path.isdir(original_dir
        ), 'error: original_dir is not a directory'
    assert isinstance(destination_dir, str) and os.path.isdir(destination_dir
        ), 'error: destination_dir is not a directory'
    assert mode == 'link' or mode == 'unlink', f'error: invalid mode: {mode}'
    if mode == 'link':
        pathlink_link(original_dir, destination_dir, tracked_paths,
            execute_linking, automatic_yes)
    elif mode == 'unlink':
        pathlink_unlink(original_dir, destination_dir, tracked_paths,
            execute_linking, automatic_yes)


def pathlink(settings_json_path, mode='link', settings_id='*',
    execute_linking=True, automatic_yes=False, silent=False):
    logging.basicConfig(level=logging.WARNING if silent and automatic_yes else
        logging.INFO, format='%(message)s')
    logging.info(f'mode = {mode}, settings_id = {settings_id}')
    logging.info(f'settings_json_path = {settings_json_path}')
    assert type(settings_json_path) is str
    assert type(mode) is str
    assert type(settings_id) is str
    assert type(execute_linking) is bool
    assert type(automatic_yes) is bool
    assert os.path.isfile(settings_json_path
        ), 'error: settings json should be a json file'
    with open(settings_json_path, 'r') as f:
        try:
            settings = json.load(f)
        except:
            raise Exception('error: settings json should be a json file')
    assert type(settings
        ) is dict, 'error: settings json should be a dictionary'
    if settings_id == '*':
        assert settings, 'error: settings json should be a non-empty dictionary'
        for s_id, s_dict in settings.items():
            pathlink_single_folder(s_id, s_dict, mode, execute_linking,
                automatic_yes)
    else:
        try:
            settings_dict = settings[settings_id]
        except:
            raise Exception(
                'error: settings json should contain an entry for the chosen id'
                )
        pathlink_single_folder(settings_id, settings_dict, mode,
            execute_linking, automatic_yes)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-m', '--mode', type=str, default='link', choices=[
        'link', 'unlink'], help=
        'chooses mode for execution: link (add symbolic links), unlink (remove symbolic links)'
        )
    parser.add_argument('-d', '--dry-run', action='store_true', help=
        'just print info without any action')
    parser.add_argument('--id', type=str, default='*', help=
        'specifies which settings id from config to link (default: * (all ids))'
        )
    SETTINGS_JSON_PATH = os.path.join(os.path.expanduser('~'),
        '.pathlinker.json')
    parser.add_argument('-s', '--settings-json', type=str, default=
        SETTINGS_JSON_PATH, help=
        'specifies file with settings (default: ~/.pathlinker.json)')
    args = parser.parse_args()
    pathlink(args.settings_json, mode=args.mode, settings_id=args.id,
        execute_linking=not args.dry_run)

#!/usr/bin/python3

import os
import shutil
import pathlib
import json
import argparse
import logging

# Helper functions
def prompt_yes_no(msg, automatic_yes=False, ask_twice=False):

    assert type(automatic_yes) is bool
    assert type(ask_twice) is bool

    if automatic_yes:
        return True
    else: 
        YES_ANSWERS = ['y', 'Y']
        if ask_twice:
            return (input(msg + " [yY/nN]: ") in YES_ANSWERS and
                input("Are you sure? [yY/nN]: ") in YES_ANSWERS)
        else:
            return input(msg + " [yY/nN]: ") in YES_ANSWERS

def pathlink_link(original_dir, destination_dir, tracked_paths, execute_linking, automatic_yes):

    for tracked_path in tracked_paths:

        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)

        logging.info(f'--- processing path: {tracked_path}')

        if os.path.exists(destination_path):
            if not os.path.exists(original_path) and not os.path.islink(original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: corresponding link DOES NOT exist')
                if execute_linking and prompt_yes_no("Do you want to add a link?", automatic_yes=automatic_yes):
                    os.symlink(destination_path, original_path)
            elif os.path.islink(original_path):
                if os.readlink(original_path) == destination_path:
                    logging.info('destination_dir: path exists')
                    logging.info(f'original_dir: symbolic link already points to correct location')
                else:
                    logging.info('destination_dir: path exists')
                    logging.info(f'original_dir: symbolic link points to WRONG location: {os.readlink(original_path)}')
                    if execute_linking and prompt_yes_no("Do you want to overwrite it?"
                            , automatic_yes=automatic_yes, ask_twice=True):
                        os.unlink(original_path)
                        os.symlink(destination_path, original_path)
            elif os.path.isdir(original_path):
                logging.info('destination_dir: path exists')
                logging.info('original_dir: folder exists (NOT a symlink)')
                if execute_linking and prompt_yes_no("Do you want to overwrite it?"
                        , automatic_yes=automatic_yes, ask_twice=True):
                    shutil.rmtree(original_path)
                    os.symlink(destination_path, original_path)
            else: # should be a file
                logging.info('destination_dir: path exists')
                logging.info('original_dir: file exists (NOT a symlink)')
                if execute_linking and prompt_yes_no("Do you want to overwrite it?"
                        , automatic_yes=automatic_yes, ask_twice=True):
                    os.remove(original_path)
                    os.symlink(destination_path, original_path)
        else: # backup path doesn't exist
            if not os.path.exists(original_path) and not os.path.islink(original_path):
                logging.info("original_dir: nothing")
                logging.info("destination_dir: nothing")
            elif os.path.islink(original_path):
                logging.info("original_dir: symbolic link exists")
                logging.info("destination_dir: nothing")
            else: # original path is a file/dir
                logging.info('original_dir: file/folder exists')
                logging.info('destination_dir: nothing')
                if execute_linking and prompt_yes_no("Do you want to move file/folder to destination_dir and add link to original_dir?", automatic_yes=automatic_yes):
                    # Move original to backup location
                    backup_path_folder = os.path.abspath(os.path.join(destination_path, '..'))
                    pathlib.Path(backup_path_folder).mkdir(parents=True, exist_ok=True)
                    shutil.move(original_path, backup_path_folder)
                    # Create symlink
                    os.symlink(destination_path, original_path)

def pathlink_unlink(original_dir, destination_dir, tracked_paths, execute_linking, automatic_yes):

    for tracked_path in tracked_paths:

        original_path = os.path.join(original_dir, tracked_path)
        destination_path = os.path.join(destination_dir, tracked_path)

        logging.info(f'--- processing path: {tracked_path}')

        if (os.path.islink(original_path) 
            and os.readlink(original_path) == destination_path
            and os.path.exists(destination_path)):
            if execute_linking and prompt_yes_no("Do you want to remove the link?", automatic_yes=automatic_yes):
                os.unlink(original_path)
                shutil.move(destination_path, original_path)
        else:
            logging.info('Not a proper link. DIY')

def pathlink_single_folder(settings_id, settings_dict, mode, execute_linking, automatic_yes):

    logging.info(f'### processing settings_id: {settings_id}')

    try:
        tracked_paths = settings_dict['tracked_paths']
        original_dir = settings_dict['original_dir']
        destination_dir = settings_dict['destination_dir']
    except:
        raise Exception('error: settings json should have fields: tracked_paths, original_dir, destination_dir')

    assert type(tracked_paths) is list, "error: tracked_paths is not a list"
    # assert tracked_paths, "error: tracked_paths should not be empty"
    assert isinstance(original_dir, str) and os.path.isdir(original_dir), "error: original_dir is not a directory"
    assert isinstance(destination_dir, str) and os.path.isdir(destination_dir), "error: destination_dir is not a directory"
    assert mode == 'link' or mode == 'unlink', f'error: invalid mode: {mode}'

    if mode == 'link':
        pathlink_link(original_dir, destination_dir, tracked_paths, execute_linking, automatic_yes)
    elif mode == 'unlink':
        pathlink_unlink(original_dir, destination_dir, tracked_paths, execute_linking, automatic_yes)


def pathlink(settings_json_path, mode='link', settings_id='*', execute_linking=True, automatic_yes=False, silent=False):

    logging.basicConfig(level=(logging.WARNING if silent and automatic_yes else logging.INFO),
        format='%(message)s')

    logging.info(f'mode = {mode}, settings_id = {settings_id}')
    logging.info(f'settings_json_path = {settings_json_path}')

    assert type(settings_json_path) is str
    assert type(mode) is str
    assert type(settings_id) is str
    assert type(execute_linking) is bool
    assert type(automatic_yes) is bool

    assert os.path.isfile(settings_json_path), 'error: settings json should be a json file'
    with open(settings_json_path, 'r') as f:
        try:
            settings = json.load(f)
        except:
            raise Exception('error: settings json should be a json file')

    assert type(settings) is dict, 'error: settings json should be a dictionary'

    if settings_id == '*':
        assert settings, 'error: settings json should be a non-empty dictionary'
        for s_id, s_dict in settings.items():
            pathlink_single_folder(s_id, s_dict, mode, execute_linking, automatic_yes)
    else: # single id to link
        try:
            settings_dict = settings[settings_id]
        except:
            raise Exception('error: settings json should contain an entry for the chosen id')
        
        pathlink_single_folder(settings_id, settings_dict, mode, execute_linking, automatic_yes)


if __name__ == "__main__":

    # Parse script arguments
    parser = argparse.ArgumentParser()

    parser.add_argument('-m', '--mode', type=str, default='link', choices=['link', 'unlink'],
        help='chooses mode for execution: link (add symbolic links), unlink (remove symbolic links)')
    parser.add_argument('-d', '--dry-run', action='store_true',
        help='just print info without any action')
    parser.add_argument('--id', type=str, default='*',
        help='specifies which settings id from config to link (default: * (all ids))')
    # default settings json file
    SETTINGS_JSON_PATH = os.path.join(os.path.expanduser('~'), '.pathlinker.json')
    parser.add_argument('-s', '--settings-json', type=str, default=SETTINGS_JSON_PATH,
        help='specifies file with settings (default: ~/.pathlinker.json)')

    args = parser.parse_args()
    
    pathlink(args.settings_json, mode=args.mode, settings_id=args.id, execute_linking=not args.dry_run)

