I've just started learning Python 3 about a month ago. I created a tool, which allows the user to quickly access some long commands.
For instance, at my day job I have to type things like these a couple times a day:
ssh -i /home/user/.ssh/id_rsa user@server
docker exec -i mysql_container_name mysql -u example -pexample example < example.sql
This really annoys me, so I created a tool which would allow me to run a ssh
or a import
and save me a lot of time.
But since I'm new to Python, I seek tips on how to improve my code.
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()
Quick config explanation
The tool allows the user to create a alias.yml
file in some directory and all the commands the user specifies there will be available in any subdirectory. The config file (alias.yml
) might contain either one command as string or a list of commands and has to look like this:
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
I introduced a possibility to use variables in %%variable%%
format in the specified commands and the user is required to specify them on run. For instance:
commands: echo: echo %%echo%%
This will require the user to type a echo Hello
to produce the output of Hello
.
And there is also a specific constant {{cwd}}
(as "config working directory) to allow the user to run path-specific commands. For example, we use php-cs-fixer
inside a specific directory and running the command invoking php-cs-fixer
will fail in any subdirectory. Thus the config has to be written as this:
command: cs: {{cwd}}/cs/php-cs-fixer --dry-run
Since this config is located in /home/user/php/project
, {{cwd}}
gets substituted with that path and then /home/user/php/project/cs/php-cs-fixer
is being executed. This allows me to run a cs
even from /home/user/php/project/code/src/Entity/etc/etc
- you got the point.
Entrypoint
Whenever a
is invoked with arguments, __main
runs run
from the above class. I wanted to be more OOP-like, so the arguments passed to run
are from sys.argv
: a.run(sys.argv[1], sys.argv[2::])
.
Conclusion
I really wonder how to improve the A
class posted above, both from the architectural and structural point of view. But if you'd like to give more tips on improving the code in overall, the repository is here.
alias
command, which allows giving a name to a command? Also,ssh
has the~/.ssh/config
file where you can configure hosts (including ports and users).\$\endgroup\$