├── examples ├── whatsmypath.json ├── my.json ├── jv.json ├── minus.json └── digitalocean.json ├── .gitignore ├── LICENSE ├── README.md └── ralph /examples/whatsmypath.json: -------------------------------------------------------------------------------- 1 | { 2 | "blueprint": "whatsmypath", 3 | "command": "echo $PATH" 4 | } 5 | -------------------------------------------------------------------------------- /examples/my.json: -------------------------------------------------------------------------------- 1 | { 2 | "blueprint": "my {*path?}", 3 | "command": "cd ~/Documents/{path}; ls", 4 | 5 | "arguments": { 6 | "path": { 7 | "values": { 8 | "default": "" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/jv.json: -------------------------------------------------------------------------------- 1 | { 2 | "blueprint": "jv {*class}", 3 | "command": "javac {class}.java; java {class}", 4 | 5 | "arguments": { 6 | "class": { 7 | "errors": { 8 | "missing": "Show me a Java source file!" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/minus.json: -------------------------------------------------------------------------------- 1 | { 2 | "blueprint": "minus {*one} {*two}", 3 | "command": "expr {one} - {two}", 4 | 5 | "arguments": { 6 | "one": { 7 | "errors": { 8 | "missing": "Where's the minuend?" 9 | } 10 | }, 11 | "two": { 12 | "errors": { 13 | "missing": "Where's the subtrahend?" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/digitalocean.json: -------------------------------------------------------------------------------- 1 | { 2 | "blueprint": "digitalocean {*user?} {~server?}", 3 | "command": "ssh {user}@{server}", 4 | 5 | "arguments": { 6 | "user": { 7 | "values": { 8 | "default": "root" 9 | } 10 | }, 11 | "server": { 12 | "values": { 13 | "default": "123.456.78.910", 14 | 15 | "website": "123.456.78.910", 16 | "git": "91.876.54.321" 17 | }, 18 | "errors": { 19 | "invalid": "I've never heard of {server}." 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 - 2019 Nimit Kalra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `ralph` 2 | ======= 3 | 4 | 5 | ``` 6 | (\ 7 | \\ 8 | )) 9 | // 10 | .-. // .-. 11 | / \-((=-/ \ /~~~~~~~~~~~~~~~~~~~~~~~~~~~+ 12 | \ \\ / / Hey there! I'm Ralph, your | 13 | $ ralph # Magic... `( ____))_ )` < personal alias apprentice. | 14 | .-' // '-. \ How can I serve you today? | 15 | / (( \ \~~~~~~~~~~~~~~~~~~~~~~~~~~~+ 16 | | ! | 17 | \ / 18 | \ |___| / 19 | _) \ / (_ 20 | (((---' '---))) 21 | ``` 22 | 23 | > Ralph helps you create Bash aliases that can be [**called with `sudo`**](#using-your-new-aliases), have [**parameters**](#design-a-blueprint), and print aesthetically-pleasing error messages. Ralph takes the work out of writing complex Bash aliases and provides parameter **restrictions** and **default** parameter values out-of-the-box. 24 | 25 | ### Design a *Blueprint* 26 | ![blueprint](https://cloud.githubusercontent.com/assets/1139621/7335747/8b64fe6a-eb98-11e4-818a-663748c3efdd.png) 27 | 28 | ### Ask Ralph to Work His *Magic* 29 | ![magic](https://cloud.githubusercontent.com/assets/1139621/7335748/8b71f5fc-eb98-11e4-97ca-34dbab476b68.png) 30 | 31 | ### Use Your New Alias! 32 | ![alias](https://cloud.githubusercontent.com/assets/1139621/7335749/8b7329b8-eb98-11e4-8858-5d4ed0ab4fff.png) 33 | 34 | ## Designing a Blueprint 35 | 36 | **Disclaimer: these instructions are a bit confusing, at the moment. It may be easier to take a look at the [example blueprints](https://github.com/qw3rtman/ralph/tree/master/examples).** 37 | 38 | A blueprint is an outline of the alias being designed. Let's take a look at the blueprint for the `digitalocean` alias shown in the images above. 39 | 40 | ```javascript 41 | { 42 | "blueprint": "digitalocean {*user?} {~server?}", 43 | ``` 44 | 45 | The first part of the blueprint, `digitalocean`, is the name of the command you want to generate. 46 | 47 | Alias parameters are defined after the first part of the blueprint. In this case, the parameters are described by `{*user?}` and `{~server?}`. Let's take a closer look at these two parameters. 48 | 49 | - `{*user?}` 50 | - the opening brace (`{`) → defines a parameter 51 | - the asterisk (`*`) → any value is acceptable (not restricted) 52 | - the parameter name (`user`) → a parameter named `user` 53 | - the question mark (`?`) → optional value, default will be used if no argument specified 54 | - the closing brace (`}`) → closes the parameter definition 55 | - `{~server?}` 56 | - the opening brace (`{`) → defines a parameter 57 | - the tilde (`~`) → certain values are acceptable (restricted) 58 | - the parameter name (`server`) → a parameter named `server` 59 | - the question mark (`?`) → optional value, default will be used if no argument specified 60 | - the closing brace (`}`) → closes the parameter definition 61 | 62 | ```javascript 63 | "command": "ssh {user}@{server}", 64 | ``` 65 | 66 | This is where you define what command is executed when you call your alias. 67 | 68 | You can use any parameters defined in the `blueprint` above in your command by enclosing the parameter name in braces, as seen in the example above. 69 | 70 | ```javascript 71 | 72 | "arguments": { 73 | "user": { 74 | "values": { 75 | "default": "root" 76 | } 77 | }, 78 | ``` 79 | 80 | Since `user` is an optional parameter (has a question mark, `?`, in the blueprint declaration), a `default` value is required. In the event that no `user` is specified, the `default` value will be passed in its place. 81 | 82 | ```javascript 83 | "server": { 84 | "values": { 85 | "default": "198.199.97.172", 86 | 87 | "website": "198.199.97.172", 88 | "git": "91.876.54.321" 89 | }, 90 | ``` 91 | 92 | Keep in mind that `server`, like `user`, is also an optional paramter, so a `default` value is required. 93 | 94 | In addition, `server` is a restricted parameter, which only allows certain, explicitly defined values to be passed. In this case, two values are defined; meaning, only these two values are acceptable parameters. 95 | 96 | `digitalocean website` and `digitalocean git` are acceptable; however, `digitalocean invalid` would print a generic error. This error can also be explcitly defined, as outlined below. 97 | 98 | ```javascript 99 | "errors": { 100 | "invalid": "I've never heard of {server}." 101 | } 102 | ``` 103 | 104 | Define an `invalid` error that will be printed if the parameter is restricted and the value provided is not acceptable. 105 | 106 | Define an `missing` error that will be printed if the paramter is **not optional** and a value is not provided. 107 | 108 | Similar to the `command`, parameters can be used by enclosing the parameter name in braces, as seen in the example above. 109 | 110 | ```javascript 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | Ensure your blueprint contains valid JSON! 117 | 118 | ## Calling Ralph 119 | After following the [simple installation instructions](#installation), just run `ralph` and Ralph will take care of everything else for you. 120 | 121 | ## Using Your New Aliases 122 | Just call them! For example, with the above example: `$ digitalocean`. 123 | 124 | In addition, you can call them with `sudo`, **unlike classic Bash aliases**: `$ sudo digitalocean`. 125 | 126 | ## Installation 127 | Installation is super-simple: no finicky package managers or dependencies (**other than Python**); just pure drag-and-drop. 128 | 129 | After downloading `ralph`, simply copy it over to your `$PATH` and you're good to go. 130 | ```sh 131 | $ wget https://github.com/qw3rtman/ralph/releases/download/v0.1.0/ralph 132 | $ chmod +x ralph 133 | $ mv ralph /usr/local/bin 134 | ``` 135 | 136 | If you don't have `wget` on your system, you can download `ralph` from the [releases page](https://github.com/qw3rtman/ralph/releases) and follow the above steps from the second one onward. 137 | 138 | **Now just run `ralph` once and Ralph will take care of setting up your `~/.ralph` directory with all the default settings.** 139 | 140 | ## Configuration 141 | All configuration for `ralph` are found in `~/.ralph/config.json`. Here, you can tell Ralph where to search for blueprint JSON files and where to place alias files after they are generated. 142 | 143 | ## Limitations 144 | The major limitation with `ralph` is inherent in optional parameters. 145 | 146 | In the above example (`digitalocean`), suppose you want to utilize the default value of `user` (the first paramter) by leaving it blank, `ralph` will just assume the first parameter is what the second parameter was intended to be. 147 | 148 | ```sh 149 | $ digitalocean website 150 | ``` 151 | 152 | Here, I intended for the `user` parameter to default to the `default` value; however, the `user` parameter would be `website` and the `server` parameter would be blank, resulting in it default to the `default` value. 153 | 154 | While I intended for the above command to result in: 155 | 156 | ```sh 157 | $ ssh root@198.199.97.172 158 | ``` 159 | 160 | It would actually result in: 161 | 162 | ```sh 163 | $ ssh website@198.199.97.172 164 | ``` 165 | 166 | ## Contributing 167 | Contributions are always welcome. 168 | 169 | Find something interesting in the TODO below, fork our code, create a new branch, and send us a pull request. 170 | 171 | There are only two rules: avoid [code smells](http://blog.codinghorror.com/code-smells/) and abide by the syntax-formatting of the existing code. 172 | 173 | ## TODO 174 | - 175 | -------------------------------------------------------------------------------- /ralph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Terminal colors. 5 | class colors: 6 | HEADER = '\033[95m' 7 | OKBLUE = '\033[94m' 8 | OKGREEN = '\033[92m' 9 | WARNING = '\033[93m' 10 | FAIL = '\033[91m' 11 | ENDC = '\033[0m' 12 | BOLD = '\033[1m' 13 | UNDERLINE = '\033[4m' 14 | 15 | import json 16 | 17 | import os.path 18 | import sys 19 | 20 | home = os.path.expanduser('~') 21 | dot_ralph = home + '/.ralph' 22 | 23 | someOperationsExecuted = False 24 | 25 | # Ensure ~/.ralph exists. 26 | if not os.path.exists(dot_ralph): 27 | print colors.OKGREEN + u'[✔] Creating ' + colors.BOLD + '~/.ralph' + colors.ENDC + colors.OKGREEN + '...' + colors.ENDC 28 | 29 | os.makedirs(dot_ralph) 30 | 31 | someOperationsExecuted = True 32 | 33 | # Ensure ~/.ralph/config.json exists. 34 | if not os.path.exists(home + '/.ralph/config.json'): 35 | print colors.OKGREEN + u'[✔] Generating default configuration in ' + colors.BOLD + '~/.ralph/config.json' + colors.ENDC + colors.OKGREEN + '...' + colors.ENDC 36 | 37 | configuration = open(home + '/.ralph/config.json', 'w+') 38 | configuration.write('{\n\t"aliases": "~/.ralph/aliases/",\n\t"blueprints": "~/.ralph/blueprints/"\n}') 39 | configuration.close() 40 | 41 | someOperationsExecuted = True 42 | 43 | # Get configuration. 44 | if os.path.exists(home + '/.ralph/config.json'): 45 | with open(home + '/.ralph/config.json') as configuration_file: 46 | configuration = json.load(configuration_file) 47 | 48 | # Ensure ~/.ralph/aliases exists. 49 | if not os.path.exists(configuration['aliases'].replace('~', home)): 50 | print colors.OKGREEN + u'[✔] Creating ' + colors.BOLD + configuration['aliases'] + colors.ENDC + colors.OKGREEN + '...' + colors.ENDC 51 | 52 | os.makedirs(configuration['aliases'].replace('~', home)) 53 | 54 | someOperationsExecuted = True 55 | 56 | # Ensure ~/.ralph/blueprints exists. 57 | if not os.path.exists(configuration['blueprints'].replace('~', home)): 58 | print colors.OKGREEN + u'[✔] Creating ' + colors.BOLD + configuration['blueprints'] + colors.ENDC + colors.OKGREEN + '...' + colors.ENDC 59 | 60 | os.makedirs(configuration['blueprints'].replace('~', home)) 61 | 62 | someOperationsExecuted = True 63 | 64 | # Add ~/.ralph/aliases to $PATH... 65 | with open(home + '/.bashrc', 'r+') as bashrc: 66 | bashrc_path = '\nPATH=' + configuration['aliases'] + ':$PATH' 67 | 68 | if bashrc_path not in bashrc.read(): 69 | bashrc.write(bashrc_path) 70 | 71 | print colors.OKGREEN + u'[✔] Adding aliases to ' + colors.BOLD + 'Bash $PATH' + colors.ENDC + colors.OKGREEN + '.' + colors.ENDC 72 | 73 | someOperationsExecuted = True 74 | 75 | with open(home + '/.zshrc', 'r+') as zshrc: 76 | zshrc_path = '\nexport PATH="' + configuration['aliases'].replace('~', home) + '":$PATH' 77 | 78 | if zshrc_path not in zshrc.read(): 79 | zshrc.write(zshrc_path) 80 | 81 | print colors.OKGREEN + u'[✔] Adding aliases to ' + colors.BOLD + 'Zsh $PATH' + colors.ENDC + colors.OKGREEN + '.' + colors.ENDC 82 | 83 | someOperationsExecuted = True 84 | 85 | # If any operations executed by now, add a new line to separate configuration 86 | # generation, etc. from the actual alias creation. 87 | if someOperationsExecuted: 88 | print 89 | 90 | # Reset someOperationsExecuted to prevent a double line break. 91 | someOperationsExecuted = False 92 | 93 | # Is the blueprint valid? 94 | def validate_blueprint(blueprint_json): 95 | try: 96 | # Actual JSON object? 97 | blueprint = json.loads(blueprint_json) 98 | except ValueError as e: 99 | error = 'Invalid JSON.' 100 | 101 | # Valid barebones blueprint? 102 | try: 103 | blueprint['blueprint'] 104 | blueprint['command'] 105 | except IndexError as e: 106 | error = 'Invalid blueprint.' 107 | 108 | # Number of arguments in blueprint equals number of arguments specified? 109 | number_of_arguments = blueprint['blueprint'].count('{') 110 | if number_of_arguments > 0: 111 | try: 112 | if len(blueprint['arguments']) != number_of_arguments: 113 | error = 'Arguments described do not match arguments in blueprint.' 114 | 115 | except KeyError as e: 116 | error = 'No arguments described.' 117 | 118 | try: 119 | error 120 | 121 | someOperationsExecuted = True 122 | 123 | print colors.FAIL + colors.BOLD + u' - [✖] Invalid blueprint in ' + colors.UNDERLINE + blueprint['blueprint'].split()[0] + colors.ENDC + colors.FAIL + colors.BOLD + '.' + colors.ENDC 124 | print colors.FAIL + ' - ' + error + colors.ENDC 125 | return False 126 | except NameError as e: 127 | return True 128 | 129 | for blueprint in os.listdir(configuration['blueprints'].replace('~', home)): 130 | with open(configuration['blueprints'].replace('~', home) + blueprint) as data_file: 131 | contents = data_file.read() 132 | 133 | try: 134 | json.loads(contents) 135 | 136 | if not validate_blueprint(contents): 137 | # Valid JSON, but not a valid blueprint. 138 | continue 139 | except ValueError as e: 140 | print colors.FAIL + colors.BOLD + u'- [✖] Invalid JSON in ' + blueprint + '.' + colors.ENDC 141 | continue # Move on to next blueprint... 142 | 143 | data = json.loads(contents) 144 | 145 | blueprint = data["blueprint"].split() 146 | 147 | alias = open(configuration['aliases'].replace('~', home) + blueprint[0], 'w') 148 | 149 | # Function declration. 150 | alias.write('#!/usr/bin/env bash\n# Generated by Ralph. (https://git.io/ralph)\n\n' + blueprint[0] + '() {') 151 | 152 | if 'arguments' in data: 153 | # Obtaining alias arguments. 154 | alias.write('\n\tparameters=(${@:1:$(($#-($#-' + str(len(data['arguments'])) + ')))})') 155 | 156 | alias.write('\n\n\tcase ${#parameters[@]} in') 157 | 158 | for number_of_arguments in range(0, len(data['arguments']) + 1): 159 | alias.write('\n\t\t' + str(number_of_arguments) + ' ) ') 160 | 161 | for index, argument in enumerate(data['arguments']): 162 | alias.write(argument + 'Set=') 163 | 164 | if index < number_of_arguments: 165 | alias.write('true') 166 | else: 167 | alias.write('false') 168 | 169 | alias.write('\n\t\t\t\t') 170 | 171 | alias.write(';;\n'); 172 | 173 | alias.write('\tesac\n') 174 | 175 | for index, argument in enumerate(data['arguments']): 176 | alias.write('\n\tif $' + argument + 'Set; then\n\t\t') 177 | if data['blueprint'][data['blueprint'].index(argument) - 1] == '*': # Any value. 178 | alias.write(argument + '=${parameters[' + str(index) + ']}') 179 | else: # Certain values. 180 | alias.write('case ${parameters[' + str(index) + ']} in') 181 | 182 | for value in data['arguments'][argument]['values']: 183 | if value != 'default': 184 | alias.write('\n\t\t\t"' + value + '" ) ' + argument + '="' + data['arguments'][argument]['values'][value] + '";;\n') 185 | 186 | invalidValueError = colors.FAIL + 'Invalid value for ' + colors.BOLD + argument + colors.ENDC + colors.FAIL + '.' 187 | if data['arguments'][argument].has_key('errors'): 188 | if data['arguments'][argument]['errors'].has_key('invalid'): 189 | invalidValueError = data['arguments'][argument]['errors']['invalid'] 190 | 191 | for invalidValueErrorArgumentIndex, invalidValueErrorArgument in enumerate(data['arguments']): 192 | invalidValueError = invalidValueError.replace('{' + invalidValueErrorArgument + '}', colors.BOLD + '${parameters[' + str(invalidValueErrorArgumentIndex) + ']}' + colors.ENDC + colors.FAIL) 193 | 194 | alias.write('\n\t\t\t* )' + ' echo "' + colors.FAIL) 195 | alias.write(u'[✖] '.encode('utf8')) 196 | alias.write(invalidValueError + '"\n\t\t\t\t\texit\n\t\t\t\t\t;;') 197 | 198 | alias.write('\n\t\tesac') 199 | 200 | if data['blueprint'][data['blueprint'].index(argument) + len(argument)] == '?': 201 | alias.write('\n\telse') 202 | alias.write('\n\t\t' + argument + '="' + data['arguments'][argument]['values']['default'] + '"') 203 | alias.write('\n\tfi') 204 | 205 | alias.write('\n') 206 | else: 207 | missingValueError = colors.FAIL + colors.BOLD + argument + colors.ENDC + colors.FAIL + u' is a required argument.' + colors.ENDC 208 | if data['arguments'][argument].has_key('errors'): 209 | if data['arguments'][argument]['errors'].has_key('missing'): 210 | missingValueError = data['arguments'][argument]['errors']['missing'] 211 | 212 | alias.write('\n\telse') 213 | alias.write('\n\t\t') 214 | alias.write('echo "' + colors.FAIL) 215 | alias.write(u'[✖] '.encode('utf8')) 216 | alias.write(missingValueError + '"') 217 | alias.write('\n\t\texit') 218 | alias.write('\n\tfi') 219 | 220 | alias.write('\n') 221 | 222 | # Generate command from blueprint and data. 223 | command = data['command'] 224 | command = command.replace('{', '$').replace('}', '') 225 | 226 | # Call the blueprint's command. 227 | alias.write('\n\t' + command) 228 | 229 | # Close the function declration. 230 | alias.write('\n}') 231 | 232 | # Call the function... 233 | alias.write('\n\n' + blueprint[0] + ' "$@"') 234 | 235 | # Trailing new line. 236 | alias.write('\n') 237 | 238 | alias.close() 239 | 240 | # Make new alias executable. 241 | new_alias = configuration['aliases'].replace('~', home) + blueprint[0]; 242 | os.chmod(new_alias, os.stat(new_alias).st_mode | 0111) 243 | 244 | print colors.OKGREEN + u'- [✔] Successfully generated alias ' + colors.BOLD + blueprint[0] + colors.ENDC + colors.ENDC + colors.OKGREEN + ' from blueprint ' + colors.BOLD + blueprint[0] + '.json' + colors.ENDC + colors.ENDC + colors.OKGREEN + '.' + colors.ENDC 245 | 246 | someOperationsExecuted = True 247 | 248 | if someOperationsExecuted: 249 | print 250 | 251 | print colors.OKGREEN + colors.BOLD + u'[✔] Done!' + colors.ENDC 252 | else: 253 | print colors.OKGREEN + u'[✔] Nothing to do. Design some blueprints in ' + colors.BOLD + configuration['blueprints'] + colors.ENDC + colors.OKGREEN + ' and run ' + colors.BOLD + 'ralph' + colors.ENDC + colors.OKGREEN + ' again to experience the magic.' + colors.ENDC 254 | --------------------------------------------------------------------------------