Инструмент для создания алиасов для команд


Я только начал изучение Python 3 около месяца назад. Я создал инструмент, который позволяет пользователю быстро получить доступ к некоторым длинных команд.

Например, в моей повседневной работе я типа что-то вроде пару раз в день:

  • ssh -i /home/user/.ssh/id_rsa user@server
  • docker exec -i mysql_container_name mysql -u example -pexample example < example.sql

Это действительно раздражает меня, поэтому я создал инструмент, который позволит мне запустить a ssh или a import и спасти меня много времени.

Но так как я новичок в Python, я ищу советы о том, как улучшить мой код.

import re
import os
import sys
import yaml
from a.Helper import Helper


class A(object):
    _argument_dict = {}
    _argument_list = []
    _settings = {}

    # Run a command
    def run(self, command, arguments):    
        self._load_config_files()
        self._validate_config_version()
        self._separate_arguments(arguments)
        self._initiate_run(command)

    # Reparse and return settigns
    def get_settings(self):
        self._load_config_files()
        self._validate_config_version()

        return self._settings

    # Load all the config files into one dictionary
    def _load_config_files(self):
        default_settings = {}
        local_settings = {}

        try:
            default_settings = self._load_config_file(os.path.dirname(__file__))
        except FileNotFoundError:
            print("Can't locate native alias.yml file, the app is corrupt. Please reinstall.")
            sys.exit()

        cwd_list = os.getcwd().split('/')

        while not cwd_list == ['']:
            path = "/".join(cwd_list)

            try:
                local_settings = self._merge_settings(local_settings, self._load_config_file(path))
            except FileNotFoundError:
                pass

            cwd_list = cwd_list[:-1]

        self._settings = self._merge_settings(default_settings, local_settings)

    # Load a specific config file from specific location
    def _load_config_file(self, path):
        with open(path + "/alias.yml", "r") as stream:
            try:
                config = yaml.load(stream)
                config = self._reparse_config_with_constants(config, path)

                return config
            except yaml.YAMLError as ex:
                print(ex)
                sys.exit()

    # Go over the configs and substitute the so-far only one constant
    def _reparse_config_with_constants(self, config, path):
        try:
            for commands in config['commands']:
                if isinstance(config['commands'][commands], str):
                    config['commands'][commands] = config['commands'][commands].replace("{{cwd}}", path)
                elif isinstance(config['commands'][commands], list):
                    for id, command in enumerate(config['commands'][commands]):
                        config['commands'][commands][id] = command.replace("{{cwd}}", path)
        except KeyError:
            pass

        return config

    # Merge the settings so that all of them are available.
    def _merge_settings(self, source, destination):
        for key, value in source.items():
            if isinstance(value, dict):
                node = destination.setdefault(key, {})
                self._merge_settings(value, node)
            else:
                destination[key] = value

        return destination

    # Parse arguments to dictionary and list. Dictionary is for named variables, list is for anonymous ones.
    def _separate_arguments(self, arguments):
        prepared_dict = []

        for argument in arguments:
            match = re.match(r"--([\w]+)=([\w\s]+)", argument)

            if match:
                prepared_dict.append((match.group(1), match.group(2)))
            else:
                self._argument_list.append(argument)

        self._argument_dict = dict(prepared_dict)

    # Die if yaml file version is not supported
    def _validate_config_version(self):
        if self._settings['version'] > self._settings['supported_version']:
            print("alias.yml version is not supported")
            sys.exit()

    # Prepare and run specific command
    def _initiate_run(self, command_name):
        try:
            command_list = self._settings['commands'][command_name]
            argument_list_cycler = iter(self._argument_list)

            # Replace a variable name with either a value from argument dictionary or from argument list
            def replace_variable_match(match):
                try:
                    return self._argument_dict[match.group(1)]
                except KeyError:
                    return next(argument_list_cycler)

            # Replace and
            def run_command(command):
                command = re.sub(r"%%([a-z_]+)%%", replace_variable_match, command)
                os.system(command)

            if isinstance(command_list, str):
                run_command(command_list)
            elif isinstance(command_list, list):
                for command in command_list:
                    run_command(command)
            else:
                Helper(self._settings)
        except StopIteration:
            print("FATAL: You did not specify the variable value for your command.")
            sys.exit()
        except IndexError:
            Helper(self._settings)
            sys.exit()
        except KeyError:
            Helper(self._settings)
            sys.exit()

Быстрая настройка объяснения

Инструмент позволяет пользователю создать alias.yml файл в какую-нибудь директорию и все команды пользователя определяет, будут доступны в любой подкаталог. Файл config (alias.yml) может содержать одну команду как строку или список команд и должна выглядеть так:

version: 1.0 # For future version, which could introduce breaking changes

commands:
  echo: echo Hello World
  ssh:
    - scp file user@remote:/home/user/file
    - ssh user@remote

Я представил возможность использовать переменные в %%variable%% формат в указанных команд, и пользователь должен указать их выполнить. Например:

commands:
  echo: echo %%echo%%

Это потребует от пользователя ввести a echo Hello для того чтобы произвести выход Hello.

А также существует постоянная {{cwd}} (как "конфигурация рабочего каталога), чтобы позволить пользователю запустить путь-определенные команды. Например, мы используем php-cs-fixer внутри определенного каталога и выполнив команду вызова php-cs-fixer будет выполнена в любой подкаталог. Таким образом, в конфиге должно быть написано, как это:

command:
  cs: {{cwd}}/cs/php-cs-fixer --dry-run

Поскольку этот объект находится в /home/user/php/project, {{cwd}} получает заменяется этот путь и затем /home/user/php/project/cs/php-cs-fixer выполняется. Это позволяет мне работать a cs даже от /home/user/php/project/code/src/Entity/etc/etc - вы попали в точку.

Точки входа

Всякий раз, когда a вызывается с аргументами, __main работает run С выше класса. Я хотела быть более ООП-как, так что аргументы, переданные run от sys.argv: a.run(sys.argv[1], sys.argv[2::]).

Заключение

Мне очень интересно, как улучшить A класс выложил выше, как с архитектурной и конструктивной точки зрения. Но если вы хотите, чтобы дать больше советов по улучшению кода в общий репозиторий находится здесь.



96
1
задан 27 января 2018 в 09:01 Источник Поделиться
Комментарии
1 ответ

В общем я вижу некоторые большие код для новичка в языке. Там совсем немного кода здесь, и у меня есть, но времени на несколько нот стиль.

Python имеет много отличных способов, чтобы выполнить итерации

Вот цикл, который действительно поймал мои глаза. Переменная итерации постоянно привык работать с данными на каждой итерации, и, следовательно, заканчивается много балласта. Это в целом вредит читабельности.

for commands in config['commands']:
if isinstance(config['commands'][commands], str):
config['commands'][commands] = config['commands'][commands].replace("{{cwd}}", path)
elif isinstance(config['commands'][commands], list):
for id, command in enumerate(config['commands'][commands]):
config['commands'][commands][id] = command.replace("{{cwd}}", path)

Постоянно reaccessing config['commands']когда вместо того, чтобы что-то вроде:

for name, command in config['commands'].items():

позволит доступ к значению по config['commands']. Это очищает код, когда это значение осуществляется:

for name, command in config['commands'].items():
if isinstance(command, str):
config['commands'][name] = command.replace("{{cwd}}", path)
elif isinstance(command, list):
command[:] = [c.replace("{{cwd}}", path) for c in command]

Но в случае изменяемых значений, можно убирать присвоение этих ценностей, а также:

command[:] = [c.replace("{{cwd}}", path) for c in command]

Примечание: Я не тестировал этот код, поэтому он может скрыть некоторые глупые опечатки.

Нет смысла в строительстве объекта, который не может быть использован

В этой конструкции (урезано):

default_settings = {}
try:
default_settings = self._load_config_file(os.path.dirname(__file__))
except FileNotFoundError:
sys.exit()

Эта линия:

default_settings = {}

является ненужным, так как он всегда будет установить в случае, когда программа не завершает работу.

Промежуточные Задачи:

Что-то вроде этого:

node = destination.setdefault(key, {})
self._merge_settings(value, node)

Я вообще предпочитаю что-то вроде:

self._merge_settings(value, destination.setdefault(key, {}))

Если имя node обеспечивает некоторые важные документации функции, или выражение является более сложным, то промежуточного назначения может сделать какой-то смысл, а если нет...

1
ответ дан 28 января 2018 в 11:01 Источник Поделиться