├── .github └── workflows │ └── pythonpublish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Pipfile ├── README.md ├── img ├── rush-run.png └── rush-view.png ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── rush_cli ├── __init__.py ├── cli.py ├── prep_tasks.py ├── read_tasks.py ├── run_tasks.py └── utils.py ├── rushfile.yml ├── script.sh └── tests ├── __init__.py ├── test_prep_tasks.py ├── test_read_tasks.py └── test_utils.py /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | tags: 9 | - v* 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | if: "!contains(github.event.head_commit.message, '[skip-ci]')" 15 | steps: 16 | - uses: actions/checkout@master 17 | - uses: actions/setup-python@v1 18 | - uses: dschep/install-poetry-action@v1.2 19 | - uses: actions/checkout@v1 20 | - uses: jpetrucciani/black-check@master 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: "3.x" 26 | 27 | - name: Run pytest 28 | run: | 29 | python3 -m venv venv 30 | source venv/bin/activate 31 | poetry install 32 | poetry run pytest 33 | 34 | - name: Build and publish 35 | env: 36 | PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} 37 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 38 | run: | 39 | poetry build 40 | poetry publish --username $PYPI_USERNAME --password $PYPI_PASSWORD 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # .pre-commit-config.yaml 2 | 3 | 4 | # reorder imports 5 | - repo: https://github.com/asottile/reorder_python_imports 6 | rev: v2.2.0 7 | hooks: 8 | - id: reorder-python-imports 9 | 10 | 11 | # black 12 | - repo: https://github.com/ambv/black 13 | rev: stable 14 | hooks: 15 | - id: black 16 | args: # arguments to configure black 17 | - --line-length=88 18 | - --include='\.pyi?$' 19 | 20 | # these folders wont be formatted by black 21 | - --exclude="""\.git | 22 | \.__pycache__| 23 | \.hg| 24 | \.mypy_cache| 25 | \.tox| 26 | \.venv| 27 | _build| 28 | buck-out| 29 | build| 30 | dist""" 31 | 32 | language_version: python3.6 33 | 34 | 35 | # flake8 36 | - repo: https://github.com/pre-commit/pre-commit-hooks 37 | rev: v2.3.0 38 | hooks: 39 | - id: flake8 40 | args: # arguments to configure flake8 41 | # making isort line length compatible with black 42 | - "--max-line-length=88" 43 | - "--max-complexity=18" 44 | - "--select=B,C,E,F,W,T4,B9" 45 | 46 | # these are errors that will be ignored by flake8 47 | # check out their meaning here 48 | # https://flake8.pycqa.org/en/latest/user/error-codes.html 49 | - "--ignore=E203,E266,E501,W503,F403,F401,E402" 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Redowan Delowar 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 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | colorama = "==0.4.3" 10 | click = "==7.0" 11 | PyYAML = "==5.2" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Rush 🏃 4 | **♆ Rush: A Minimalistic Bash Utility** 5 | 6 | 7 | ![img](./img/rush-run.png) 8 | 9 | **Run all your task automation **Bash commands** from a single `rushfile.yml` file.** 10 |
11 | 12 | 13 | ## Features 14 | * Supports all **bash** commands 15 | * Option to ignore or run specific tasks 16 | * By default, runs commands in **interactive** mode 17 | * Option to catch or ignore **command errors** 18 | * Option to show or supress **command outputs** 19 | * **Command chaining is supported** (See the example `rushfile.yml` where `task_2` is chained to `task_1`) 20 | 21 | ## Installation 22 | 23 | ``` 24 | $ pip3 install rush-cli 25 | ``` 26 | 27 | ## Workflow 28 | 29 | ### Rushfile 30 | Here is an example `rushfile.yml`. It needs to reside in the root directory: 31 | 32 | ``` yml 33 | # rushfile.yml 34 | 35 | task_1: | 36 | echo "task1 is running" 37 | 38 | task_2: | 39 | # Task chaining [task_1 is a dependency of task_2] 40 | task_1 41 | echo "task2 is running" 42 | 43 | task_3: | 44 | ls -a 45 | sudo apt-get install cowsay | head -n 0 46 | cowsay "Around the world in 80 days!" 47 | 48 | //task_4: | 49 | # Ignoring a task [task_4 will be ignored while execution] 50 | ls | grep "ce" 51 | ls > he.txt1 52 | 53 | task_5: | 54 | # Running a bash script from rush 55 | ./script.sh 56 | ``` 57 | 58 | ### Available Options 59 | To see all the available options, run: 60 | ``` 61 | $ rush 62 | ``` 63 | or, 64 | ``` 65 | $ rush --help 66 | ``` 67 | This should show: 68 | 69 | ``` 70 | Usage: rush [OPTIONS] [FILTER_NAMES]... 71 | 72 | ♆ Rush: A Minimalistic Bash Utility 73 | 74 | Options: 75 | -a, --all Run all tasks 76 | --hide-outputs Option to hide interactive output 77 | --ignore-errors Option to ignore errors 78 | -p, --path Show the absolute path of rushfile.yml 79 | --no-deps Do not run dependent tasks 80 | --view-tasks View task commands 81 | -ls, --list-tasks List task commands with dependencies 82 | --no-warns Do not show warnings 83 | -v, --version Show rush version 84 | -h, --help Show this message and exit. 85 | ``` 86 | 87 | ### Running Tasks 88 | 89 | * **Run all the tasks** 90 | ``` 91 | $ rush --all 92 | ``` 93 | 94 | * **Run specific tasks** 95 | ``` 96 | $ rush task_1 task_4 97 | ``` 98 | * **Ignore specific tasks** 99 | 100 | See the example `rushfile.yml` where the `'//'` before a task name means that the task will be ignored during execution 101 | 102 | ``` 103 | # rushfile.yml 104 | 105 | //task_4: | 106 | echo "This task will be ignored during execution." 107 | ``` 108 | This ignores the task named `//task_4`. 109 | 110 | * **Run tasks non interactively** (supress the outputs) 111 | ``` 112 | $ rush --hide-outputs 113 | ``` 114 | 115 | * **Run tasks ignoring errors** 116 | ``` 117 | $ rush --ignore-errors 118 | ``` 119 | 120 | * **Do not run the dependent tasks** 121 | ``` 122 | $ rush task_2 --no-deps 123 | ``` 124 | 125 | ### Viewing Tasks 126 | 127 | * **View absolute path of rushfile.yml** 128 | ``` 129 | $ rush --path 130 | ``` 131 | output, 132 | ``` 133 | /home/rednafi/code/rush/rushfile.yml 134 | ``` 135 | 136 | * **View task commands** 137 | ``` 138 | $ rush task_5 task_6 task_7 --view-tasks 139 | ``` 140 | ![img](./img/rush-view.png) 141 | 142 | * **View task list with dependencies** 143 | ``` 144 | $ rush -ls 145 | ``` 146 | 147 | ## Quirks 148 | 149 | * Rush runs all the commands using `/usr/bin/bash`. So shell specific syntax with other shebangs might throw error. 150 | 151 | * If you are running Bash script from rush, use shebang (`#!/usr/bin/env bash`) 152 | 153 | 154 | ## Issues 155 | * Rush works better with python 3.7 and up 156 | * If your have installed `Rush` globally and it throws a runtime error, you can try to solve it via adding the following variables to your `~./bashrc`: 157 | 158 | ``` 159 | export LC_ALL=C.UTF-8 160 | export LANG=C.UTF-8 161 | ``` 162 | You can find more information about the issue and why it's a non-trivial problem [here.](http://click.palletsprojects.com/en/7.x/python3/#python-3-surrogate-handling) 163 | -------------------------------------------------------------------------------- /img/rush-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednafi/rush/216d0a8f85ec90853608de4226fb61b2d287cf9f/img/rush-run.png -------------------------------------------------------------------------------- /img/rush-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednafi/rush/216d0a8f85ec90853608de4226fb61b2d287cf9f/img/rush-view.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "Atomic file writes." 4 | marker = "sys_platform == \"win32\"" 5 | name = "atomicwrites" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | version = "1.3.0" 9 | 10 | [[package]] 11 | category = "dev" 12 | description = "Classes Without Boilerplate" 13 | name = "attrs" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | version = "19.3.0" 17 | 18 | [package.extras] 19 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 20 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 21 | docs = ["sphinx", "zope.interface"] 22 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 23 | 24 | [[package]] 25 | category = "main" 26 | description = "Composable command line interface toolkit" 27 | name = "click" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 30 | version = "7.0" 31 | 32 | [[package]] 33 | category = "main" 34 | description = "Colorization of help messages in Click" 35 | name = "click-help-colors" 36 | optional = false 37 | python-versions = "*" 38 | version = "0.6" 39 | 40 | [package.dependencies] 41 | click = ">=7.0" 42 | 43 | [[package]] 44 | category = "main" 45 | description = "Cross-platform colored terminal text." 46 | name = "colorama" 47 | optional = false 48 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 49 | version = "0.4.3" 50 | 51 | [[package]] 52 | category = "dev" 53 | description = "Code coverage measurement for Python" 54 | name = "coverage" 55 | optional = false 56 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 57 | version = "5.0.3" 58 | 59 | [package.extras] 60 | toml = ["toml"] 61 | 62 | [[package]] 63 | category = "dev" 64 | description = "Read metadata from Python packages" 65 | marker = "python_version < \"3.8\"" 66 | name = "importlib-metadata" 67 | optional = false 68 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 69 | version = "1.4.0" 70 | 71 | [package.dependencies] 72 | zipp = ">=0.5" 73 | 74 | [package.extras] 75 | docs = ["sphinx", "rst.linker"] 76 | testing = ["packaging", "importlib-resources"] 77 | 78 | [[package]] 79 | category = "dev" 80 | description = "Rolling backport of unittest.mock for all Pythons" 81 | name = "mock" 82 | optional = false 83 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 84 | version = "3.0.5" 85 | 86 | [package.dependencies] 87 | six = "*" 88 | 89 | [package.extras] 90 | build = ["twine", "wheel", "blurb"] 91 | docs = ["sphinx"] 92 | test = ["pytest", "pytest-cov"] 93 | 94 | [[package]] 95 | category = "dev" 96 | description = "More routines for operating on iterables, beyond itertools" 97 | name = "more-itertools" 98 | optional = false 99 | python-versions = ">=3.5" 100 | version = "8.1.0" 101 | 102 | [[package]] 103 | category = "dev" 104 | description = "Core utilities for Python packages" 105 | name = "packaging" 106 | optional = false 107 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 108 | version = "20.0" 109 | 110 | [package.dependencies] 111 | pyparsing = ">=2.0.2" 112 | six = "*" 113 | 114 | [[package]] 115 | category = "dev" 116 | description = "Object-oriented filesystem paths" 117 | marker = "python_version < \"3.6\"" 118 | name = "pathlib2" 119 | optional = false 120 | python-versions = "*" 121 | version = "2.3.5" 122 | 123 | [package.dependencies] 124 | six = "*" 125 | 126 | [[package]] 127 | category = "dev" 128 | description = "plugin and hook calling mechanisms for python" 129 | name = "pluggy" 130 | optional = false 131 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 132 | version = "0.13.1" 133 | 134 | [package.dependencies] 135 | [package.dependencies.importlib-metadata] 136 | python = "<3.8" 137 | version = ">=0.12" 138 | 139 | [package.extras] 140 | dev = ["pre-commit", "tox"] 141 | 142 | [[package]] 143 | category = "dev" 144 | description = "Prettifies Python exception output to make it legible." 145 | name = "pretty-errors" 146 | optional = false 147 | python-versions = "*" 148 | version = "1.2.10" 149 | 150 | [package.dependencies] 151 | colorama = "*" 152 | 153 | [[package]] 154 | category = "dev" 155 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 156 | name = "py" 157 | optional = false 158 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 159 | version = "1.8.1" 160 | 161 | [[package]] 162 | category = "main" 163 | description = "Pygments is a syntax highlighting package written in Python." 164 | name = "pygments" 165 | optional = false 166 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 167 | version = "2.5.2" 168 | 169 | [[package]] 170 | category = "dev" 171 | description = "Python parsing module" 172 | name = "pyparsing" 173 | optional = false 174 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 175 | version = "2.4.6" 176 | 177 | [[package]] 178 | category = "dev" 179 | description = "pytest: simple powerful testing with Python" 180 | name = "pytest" 181 | optional = false 182 | python-versions = ">=3.5" 183 | version = "5.3.4" 184 | 185 | [package.dependencies] 186 | atomicwrites = ">=1.0" 187 | attrs = ">=17.4.0" 188 | colorama = "*" 189 | more-itertools = ">=4.0.0" 190 | packaging = "*" 191 | pluggy = ">=0.12,<1.0" 192 | py = ">=1.5.0" 193 | wcwidth = "*" 194 | 195 | [package.dependencies.importlib-metadata] 196 | python = "<3.8" 197 | version = ">=0.12" 198 | 199 | [package.dependencies.pathlib2] 200 | python = "<3.6" 201 | version = ">=2.2.0" 202 | 203 | [package.extras] 204 | checkqa-mypy = ["mypy (v0.761)"] 205 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 206 | 207 | [[package]] 208 | category = "dev" 209 | description = "Pytest plugin for measuring coverage." 210 | name = "pytest-cov" 211 | optional = false 212 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 213 | version = "2.8.1" 214 | 215 | [package.dependencies] 216 | coverage = ">=4.4" 217 | pytest = ">=3.6" 218 | 219 | [package.extras] 220 | testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] 221 | 222 | [[package]] 223 | category = "dev" 224 | description = "A plugin to fake subprocess for pytest" 225 | name = "pytest-subprocess" 226 | optional = false 227 | python-versions = ">=3.4" 228 | version = "0.1.2" 229 | 230 | [package.dependencies] 231 | pytest = ">=4.0.0" 232 | 233 | [package.extras] 234 | dev = ["nox", "changelogd"] 235 | docs = ["sphinx", "sphinxcontrib-napoleon", "sphinx-autodoc-typehints", "changelogd"] 236 | test = ["pytest (>=4.0)", "coverage", "docutils (>=0.12)", "Pygments (>=2.0)", "pytest-azurepipelines"] 237 | 238 | [[package]] 239 | category = "main" 240 | description = "YAML parser and emitter for Python" 241 | name = "pyyaml" 242 | optional = false 243 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 244 | version = "5.3" 245 | 246 | [[package]] 247 | category = "dev" 248 | description = "Python 2 and 3 compatibility utilities" 249 | name = "six" 250 | optional = false 251 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 252 | version = "1.14.0" 253 | 254 | [[package]] 255 | category = "dev" 256 | description = "Measures number of Terminal column cells of wide-character codes" 257 | name = "wcwidth" 258 | optional = false 259 | python-versions = "*" 260 | version = "0.1.8" 261 | 262 | [[package]] 263 | category = "dev" 264 | description = "Backport of pathlib-compatible object wrapper for zip files" 265 | marker = "python_version < \"3.8\"" 266 | name = "zipp" 267 | optional = false 268 | python-versions = ">=2.7" 269 | version = "1.0.0" 270 | 271 | [package.dependencies] 272 | more-itertools = "*" 273 | 274 | [package.extras] 275 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 276 | testing = ["pathlib2", "contextlib2", "unittest2"] 277 | 278 | [metadata] 279 | content-hash = "ce0106bb313726b61e80095174da9d4c3094dd1a5a7aa0c5fd01ed8957ad6323" 280 | python-versions = "^3.5" 281 | 282 | [metadata.files] 283 | atomicwrites = [ 284 | {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, 285 | {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, 286 | ] 287 | attrs = [ 288 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 289 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 290 | ] 291 | click = [ 292 | {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, 293 | {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, 294 | ] 295 | click-help-colors = [ 296 | {file = "click-help-colors-0.6.tar.gz", hash = "sha256:258d5f4d79e54af8d017c07313456db22e636c964dd0808a2fb0aefc654ee30c"}, 297 | {file = "click_help_colors-0.6-py3-none-any.whl", hash = "sha256:979b3837da6c6cfccd59f4f20e28ff06c6fc4c240c7d2660b3a2c2b337ae5dcb"}, 298 | ] 299 | colorama = [ 300 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 301 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 302 | ] 303 | coverage = [ 304 | {file = "coverage-5.0.3-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f"}, 305 | {file = "coverage-5.0.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc"}, 306 | {file = "coverage-5.0.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a"}, 307 | {file = "coverage-5.0.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52"}, 308 | {file = "coverage-5.0.3-cp27-cp27m-win32.whl", hash = "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c"}, 309 | {file = "coverage-5.0.3-cp27-cp27m-win_amd64.whl", hash = "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73"}, 310 | {file = "coverage-5.0.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68"}, 311 | {file = "coverage-5.0.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691"}, 312 | {file = "coverage-5.0.3-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301"}, 313 | {file = "coverage-5.0.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf"}, 314 | {file = "coverage-5.0.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3"}, 315 | {file = "coverage-5.0.3-cp35-cp35m-win32.whl", hash = "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"}, 316 | {file = "coverage-5.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0"}, 317 | {file = "coverage-5.0.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2"}, 318 | {file = "coverage-5.0.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894"}, 319 | {file = "coverage-5.0.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf"}, 320 | {file = "coverage-5.0.3-cp36-cp36m-win32.whl", hash = "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477"}, 321 | {file = "coverage-5.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc"}, 322 | {file = "coverage-5.0.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8"}, 323 | {file = "coverage-5.0.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987"}, 324 | {file = "coverage-5.0.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea"}, 325 | {file = "coverage-5.0.3-cp37-cp37m-win32.whl", hash = "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc"}, 326 | {file = "coverage-5.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e"}, 327 | {file = "coverage-5.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb"}, 328 | {file = "coverage-5.0.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37"}, 329 | {file = "coverage-5.0.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d"}, 330 | {file = "coverage-5.0.3-cp38-cp38m-win32.whl", hash = "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954"}, 331 | {file = "coverage-5.0.3-cp38-cp38m-win_amd64.whl", hash = "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e"}, 332 | {file = "coverage-5.0.3-cp39-cp39m-win32.whl", hash = "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40"}, 333 | {file = "coverage-5.0.3-cp39-cp39m-win_amd64.whl", hash = "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af"}, 334 | {file = "coverage-5.0.3.tar.gz", hash = "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef"}, 335 | ] 336 | importlib-metadata = [ 337 | {file = "importlib_metadata-1.4.0-py2.py3-none-any.whl", hash = "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359"}, 338 | {file = "importlib_metadata-1.4.0.tar.gz", hash = "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"}, 339 | ] 340 | mock = [ 341 | {file = "mock-3.0.5-py2.py3-none-any.whl", hash = "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"}, 342 | {file = "mock-3.0.5.tar.gz", hash = "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3"}, 343 | ] 344 | more-itertools = [ 345 | {file = "more-itertools-8.1.0.tar.gz", hash = "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"}, 346 | {file = "more_itertools-8.1.0-py3-none-any.whl", hash = "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39"}, 347 | ] 348 | packaging = [ 349 | {file = "packaging-20.0-py2.py3-none-any.whl", hash = "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb"}, 350 | {file = "packaging-20.0.tar.gz", hash = "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"}, 351 | ] 352 | pathlib2 = [ 353 | {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, 354 | {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, 355 | ] 356 | pluggy = [ 357 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 358 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 359 | ] 360 | pretty-errors = [ 361 | {file = "pretty_errors-1.2.10-py3-none-any.whl", hash = "sha256:2b94f96cff95007326ca5c8bde2e88d7f92d1950e023008707f19c4c02e42d10"}, 362 | {file = "pretty_errors-1.2.10.tar.gz", hash = "sha256:2be316c71f0d856d272c797f3dea6ceb83c66c9f90d64e1e9382fcdab544b019"}, 363 | ] 364 | py = [ 365 | {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, 366 | {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, 367 | ] 368 | pygments = [ 369 | {file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, 370 | {file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, 371 | ] 372 | pyparsing = [ 373 | {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"}, 374 | {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, 375 | ] 376 | pytest = [ 377 | {file = "pytest-5.3.4-py3-none-any.whl", hash = "sha256:c13d1943c63e599b98cf118fcb9703e4d7bde7caa9a432567bcdcae4bf512d20"}, 378 | {file = "pytest-5.3.4.tar.gz", hash = "sha256:1d122e8be54d1a709e56f82e2d85dcba3018313d64647f38a91aec88c239b600"}, 379 | ] 380 | pytest-cov = [ 381 | {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, 382 | {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, 383 | ] 384 | pytest-subprocess = [ 385 | {file = "pytest-subprocess-0.1.2.tar.gz", hash = "sha256:b6c91060a36d3bbc65c84d84f9223546fad3667c2b62a744baf257f100a3a8ad"}, 386 | {file = "pytest_subprocess-0.1.2-py3-none-any.whl", hash = "sha256:f43b73cca81dd25525ea09230561d32bf9c19193564e32adb9d99e9b93aec196"}, 387 | ] 388 | pyyaml = [ 389 | {file = "PyYAML-5.3-cp27-cp27m-win32.whl", hash = "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d"}, 390 | {file = "PyYAML-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6"}, 391 | {file = "PyYAML-5.3-cp35-cp35m-win32.whl", hash = "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e"}, 392 | {file = "PyYAML-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689"}, 393 | {file = "PyYAML-5.3-cp36-cp36m-win32.whl", hash = "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994"}, 394 | {file = "PyYAML-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e"}, 395 | {file = "PyYAML-5.3-cp37-cp37m-win32.whl", hash = "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5"}, 396 | {file = "PyYAML-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf"}, 397 | {file = "PyYAML-5.3-cp38-cp38-win32.whl", hash = "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811"}, 398 | {file = "PyYAML-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20"}, 399 | {file = "PyYAML-5.3.tar.gz", hash = "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"}, 400 | ] 401 | six = [ 402 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 403 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 404 | ] 405 | wcwidth = [ 406 | {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, 407 | {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, 408 | ] 409 | zipp = [ 410 | {file = "zipp-1.0.0-py2.py3-none-any.whl", hash = "sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656"}, 411 | {file = "zipp-1.0.0.tar.gz", hash = "sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"}, 412 | ] 413 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "rush-cli" 3 | version = "0.6.1" 4 | description = "♆ Rush: A Minimalistic Bash Utility" 5 | authors = ["rednafi "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/rednafi/rush" 9 | keywords = ["cli", "bash", "task", "manager", "runner"] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.5" 13 | click = "^7.0" 14 | colorama = "^0.4.3" 15 | pyyaml = "^5.2" 16 | pygments = "^2.5.2" 17 | click_help_colors = "^0.6" 18 | 19 | [tool.poetry.scripts] 20 | rush = "rush_cli.cli:entrypoint" 21 | 22 | [tool.poetry.dev-dependencies] 23 | pytest = "^5.3.2" 24 | mock = "^3.0.5" 25 | pretty_errors = "^1.2.7" 26 | pytest-cov = "^2.8.1" 27 | pytest-subprocess = "^0.1.1" 28 | 29 | [build-system] 30 | requires = ["poetry>=0.12"] 31 | build-backend = "poetry.masonry.api" 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | colorama==0.4.3 3 | Click==7.0 4 | PyYAML==5.2 5 | -------------------------------------------------------------------------------- /rush_cli/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.6.1" 2 | -------------------------------------------------------------------------------- /rush_cli/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | import colorama 5 | from click_help_colors import HelpColorsCommand 6 | 7 | from rush_cli import __version__ 8 | from rush_cli.prep_tasks import Views 9 | from rush_cli.read_tasks import ReadTasks 10 | from rush_cli.run_tasks import RunTasks 11 | 12 | # Don't strip colors. 13 | colorama.init(strip=False) 14 | 15 | VERSION = __version__ 16 | 17 | 18 | @click.command( 19 | context_settings=dict( 20 | help_option_names=["-h", "--help"], token_normalize_func=lambda x: x.lower() 21 | ), 22 | cls=HelpColorsCommand, 23 | help_headers_color="yellow", 24 | help_options_color="cyan", 25 | ) 26 | @click.option("--all", "-a", is_flag=True, multiple=True, help="Run all tasks") 27 | @click.option( 28 | "--hide-outputs", 29 | is_flag=True, 30 | default=True, 31 | help="Option to hide interactive output", 32 | ) 33 | @click.option("--ignore-errors", is_flag=True, help="Option to ignore errors") 34 | @click.option( 35 | "--path", 36 | "-p", 37 | is_flag=True, 38 | default=None, 39 | help="Show the absolute path of rushfile.yml", 40 | ) 41 | @click.option("--no-deps", is_flag=True, help="Do not run dependent tasks") 42 | @click.option("--view-tasks", is_flag=True, help="View task commands") 43 | @click.option( 44 | "--list-tasks", 45 | "-ls", 46 | is_flag=True, 47 | default=None, 48 | help="List task commands with dependencies", 49 | ) 50 | @click.option("--no-warns", is_flag=True, help="Do not show warnings") 51 | @click.option("--version", "-v", is_flag=True, help="Show rush version") 52 | @click.argument("filter_names", required=False, nargs=-1) 53 | def entrypoint( 54 | *, 55 | filter_names, 56 | all, 57 | hide_outputs, 58 | ignore_errors, 59 | path, 60 | no_deps, 61 | version, 62 | view_tasks, 63 | list_tasks, 64 | no_warns, 65 | ): 66 | """♆ Rush: A Minimalistic Bash Utility""" 67 | 68 | if len(sys.argv) == 1: 69 | entrypoint.main(["-h"]) 70 | 71 | elif path: 72 | views_obj = Views() 73 | views_obj.view_rushpath 74 | 75 | elif view_tasks: 76 | views_obj = Views(*filter_names) 77 | views_obj.view_tasks 78 | 79 | elif list_tasks: 80 | views_obj = Views(*filter_names) 81 | views_obj.view_tasklist 82 | 83 | elif version: 84 | click.secho(f"Rush version: {VERSION}", fg="cyan") 85 | 86 | elif filter_names: 87 | run_tasks_obj = RunTasks( 88 | *filter_names, 89 | show_outputs=hide_outputs, 90 | catch_errors=ignore_errors, 91 | no_deps=no_deps, 92 | no_warns=no_warns, 93 | ) 94 | run_tasks_obj.run_all_tasks() 95 | 96 | elif all: 97 | run_tasks_obj = RunTasks( 98 | show_outputs=hide_outputs, 99 | catch_errors=ignore_errors, 100 | no_deps=no_deps, 101 | no_warns=no_warns, 102 | ) 103 | run_tasks_obj.run_all_tasks() 104 | -------------------------------------------------------------------------------- /rush_cli/prep_tasks.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from collections import OrderedDict 3 | 4 | import click 5 | 6 | from rush_cli.read_tasks import ReadTasks 7 | from rush_cli.utils import beautify_task_cmd 8 | from rush_cli.utils import beautify_task_name 9 | from rush_cli.utils import scream 10 | 11 | 12 | class PrepTasks(ReadTasks): 13 | """Class for preprocessing tasks before running.""" 14 | 15 | def __init__(self, *args, no_deps=False, **kwargs): 16 | super().__init__(**kwargs) 17 | self.filter_names = args 18 | self.no_deps = no_deps 19 | 20 | @staticmethod 21 | def _clean_tasks(yml_content): 22 | """Splitting stringified tasks into into a list of individual tasks.""" 23 | 24 | cleaned_tasks = OrderedDict() 25 | 26 | for task_name, task_chunk in yml_content.items(): 27 | if task_chunk: 28 | task_chunk = task_chunk.rstrip() 29 | task_chunk = task_chunk.split("\n") 30 | cleaned_tasks[task_name] = task_chunk 31 | else: 32 | cleaned_tasks[task_name] = "" 33 | 34 | return cleaned_tasks 35 | 36 | def _replace_placeholder_tasks(self, task_chunk: list, cleaned_tasks: dict) -> list: 37 | """Recursively replace dependant task names with actual task commands.""" 38 | 39 | for idx, task in enumerate(task_chunk): 40 | if isinstance(task, str): 41 | if task in cleaned_tasks.keys(): 42 | if not self.no_deps: 43 | task_chunk[idx] = cleaned_tasks[task] 44 | else: 45 | task_chunk[idx] = "" 46 | else: 47 | task_chunk[idx] = PrepTasks._replace_placeholder_tasks( 48 | task, cleaned_tasks 49 | ) 50 | 51 | return task_chunk 52 | 53 | @classmethod 54 | def _flatten_task_chunk(cls, nested_task_chunk: list) -> list: 55 | """Recursively converts a nested task list to a flat list.""" 56 | 57 | flat_task_chunk = [] 58 | for elem in nested_task_chunk: 59 | if isinstance(elem, list): 60 | flat_task_chunk.extend(cls._flatten_task_chunk(elem)) 61 | else: 62 | flat_task_chunk.append(elem) 63 | return flat_task_chunk 64 | 65 | @staticmethod 66 | def _filter_tasks(cleaned_tasks: dict, *filter_names) -> dict: 67 | """Filter tasks selected by the user.""" 68 | 69 | if filter_names: 70 | try: 71 | filtered_tasks = {k: cleaned_tasks[k] for k in filter_names} 72 | return filtered_tasks 73 | 74 | except KeyError: 75 | not_found_tasks = [ 76 | k for k in filter_names if k not in cleaned_tasks.keys() 77 | ] 78 | click.secho( 79 | f"Error: Tasks {not_found_tasks} do not exist.", fg="magenta" 80 | ) 81 | sys.exit(1) 82 | else: 83 | return cleaned_tasks 84 | 85 | def get_prepared_tasks(self): 86 | """Get the preprocessed task dict.""" 87 | 88 | yml_content = super().read_rushfile() 89 | cleaned_tasks = self._clean_tasks(yml_content) 90 | 91 | # replace placeholders and flatten 92 | for task_name, task_chunk in cleaned_tasks.items(): 93 | task_chunk = self._replace_placeholder_tasks(task_chunk, cleaned_tasks) 94 | task_chunk = self._flatten_task_chunk(task_chunk) 95 | task_chunk = "\n".join(task_chunk) 96 | cleaned_tasks[task_name] = task_chunk 97 | 98 | # apply filter 99 | cleaned_tasks = self._filter_tasks(cleaned_tasks, *self.filter_names) 100 | return cleaned_tasks 101 | 102 | 103 | class Views(PrepTasks): 104 | """View ad hoc tasks.""" 105 | 106 | def __init__(self, *args, **kwargs): 107 | super().__init__(*args, **kwargs) 108 | self.filter_names = args 109 | 110 | @property 111 | def view_rushpath(self): 112 | rushfile_path = self.find_rushfile() 113 | click.secho(rushfile_path, fg="cyan") 114 | 115 | @property 116 | def view_tasks(self): 117 | cleaned_tasks = self.get_prepared_tasks() 118 | 119 | scream(what="view") 120 | for k, v in cleaned_tasks.items(): 121 | beautify_task_name(k) 122 | beautify_task_cmd(v) 123 | 124 | @property 125 | def view_tasklist(self): 126 | deps = self._prep_deps() 127 | 128 | scream(what="list") 129 | click.echo() 130 | for k, v in deps.items(): 131 | click.secho("-" + " " + k, fg="yellow") 132 | for cmd in v: 133 | click.echo(" " * 2 + "-" + " " + cmd) 134 | 135 | def _prep_deps(self): 136 | """Preparing a dependency dict from yml contents.""" 137 | 138 | # reading raw rushfile as a dict 139 | yml_content = self.read_rushfile() 140 | 141 | # splitting dict values by newlines 142 | yml_content = {k: v.split("\n") for k, v in yml_content.items() if v} 143 | 144 | # finding task dependencies 145 | deps = {} 146 | for k, v in yml_content.items(): 147 | lst = [] 148 | for cmd in v: 149 | if cmd in yml_content.keys(): 150 | lst.append(cmd) 151 | deps[k] = lst 152 | 153 | # filter dependencies 154 | deps = self._filter_tasks(deps, *self.filter_names) 155 | 156 | return deps 157 | -------------------------------------------------------------------------------- /rush_cli/read_tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import click 5 | import yaml 6 | 7 | from rush_cli.utils import check_pipe 8 | from rush_cli.utils import find_shell_path 9 | from rush_cli.utils import walk_up 10 | 11 | 12 | class ReadTasks: 13 | """Class for preprocessing tasks before running.""" 14 | 15 | def __init__( 16 | self, 17 | use_shell=find_shell_path("bash"), 18 | filename="rushfile.yml", 19 | current_dir=os.getcwd(), 20 | no_warns=False, 21 | ): 22 | self.use_shell = use_shell 23 | self.filename = filename 24 | self.current_dir = current_dir 25 | self.no_warns = no_warns 26 | 27 | def find_rushfile(self, max_depth=4, topdown=False): 28 | """Returns the path of a rushfile in parent directories.""" 29 | 30 | i = 0 31 | for c, d, f in walk_up(self.current_dir): 32 | if i > max_depth: 33 | break 34 | elif self.filename in f: 35 | return os.path.join(c, self.filename) 36 | i += 1 37 | 38 | click.secho("Error: rushfile.yml not found.", fg="magenta") 39 | sys.exit(1) 40 | 41 | def read_rushfile(self): 42 | 43 | rushfile = self.find_rushfile() 44 | try: 45 | with open(rushfile) as file: 46 | yml_content = yaml.load(file, Loader=yaml.SafeLoader) 47 | 48 | # make sure the task names are strings 49 | yml_content = {str(k): v for k, v in yml_content.items()} 50 | 51 | # if pipe is missing then raise exception 52 | check_pipe(yml_content, no_warns=self.no_warns) 53 | 54 | return yml_content 55 | 56 | except (yaml.scanner.ScannerError, yaml.parser.ParserError): 57 | click.secho("Error: rushfile.yml is not properly formatted", fg="magenta") 58 | sys.exit(1) 59 | 60 | except AttributeError: 61 | click.secho("Error: rushfile.yml is empty", fg="magenta") 62 | sys.exit(1) 63 | 64 | 65 | # from pprint import pprint 66 | 67 | # obj = ReadTasks() 68 | # pprint(obj.read_rushfile()["//task_4"]) 69 | -------------------------------------------------------------------------------- /rush_cli/run_tasks.py: -------------------------------------------------------------------------------- 1 | from rush_cli.prep_tasks import PrepTasks 2 | from rush_cli.utils import beautify_skiptask_name 3 | from rush_cli.utils import run_task 4 | from rush_cli.utils import scream 5 | 6 | 7 | class RunTasks(PrepTasks): 8 | """Class for running the cleaned, flattened & filtered tasks.""" 9 | 10 | def __init__(self, *args, show_outputs=True, catch_errors=True, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | self.show_outputs = show_outputs 13 | self.catch_errors = catch_errors 14 | self.no_deps = kwargs.get("no_deps", False) 15 | 16 | def run_all_tasks(self): 17 | cleaned_tasks = super().get_prepared_tasks() 18 | scream(what="run") 19 | for task_name, task_chunk in cleaned_tasks.items(): 20 | 21 | if not task_name.startswith("//"): 22 | run_task( 23 | task_chunk, 24 | task_name, 25 | interactive=self.show_outputs, 26 | catch_errors=self.catch_errors, 27 | ) 28 | else: 29 | beautify_skiptask_name(task_name) 30 | -------------------------------------------------------------------------------- /rush_cli/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | 5 | import click 6 | from pygments import highlight 7 | from pygments.formatters import TerminalFormatter 8 | from pygments.lexers import BashLexer 9 | 10 | 11 | def walk_up(bottom): 12 | """mimic os.walk, but walk 'up' instead of down the directory tree. 13 | From: https://gist.github.com/zdavkeos/1098474 14 | """ 15 | 16 | bottom = os.path.realpath(bottom) 17 | 18 | # get files in current dir 19 | try: 20 | names = os.listdir(bottom) 21 | except Exception: 22 | return 23 | 24 | dirs, nondirs = [], [] 25 | for name in names: 26 | if os.path.isdir(os.path.join(bottom, name)): 27 | dirs.append(name) 28 | else: 29 | nondirs.append(name) 30 | 31 | yield bottom, dirs, nondirs 32 | 33 | new_path = os.path.realpath(os.path.join(bottom, "..")) 34 | 35 | # see if we are at the top 36 | if new_path == bottom: 37 | return 38 | 39 | for x in walk_up(new_path): 40 | yield x 41 | 42 | 43 | def check_pipe(yml_content, no_warns=False): 44 | """Check if there is a pipe ('|') after each task name. 45 | Raise exception if pipe is missing.""" 46 | 47 | for task_name, task_chunk in yml_content.items(): 48 | if task_chunk: 49 | if not task_chunk.endswith("\n") and not no_warns: 50 | click.secho( 51 | f"Warning: Pipe (|) after {task_name} is missing", fg="yellow" 52 | ) 53 | 54 | 55 | def beautify_task_name(task_name): 56 | click.echo() 57 | task_name = f"{task_name}:" 58 | underline_len = len(task_name) + 3 59 | underline = "=" * underline_len 60 | 61 | task_name = str(click.style(task_name, fg="yellow")) 62 | underline = str(click.style(underline, fg="green")) 63 | 64 | click.echo(task_name) 65 | click.echo(underline) 66 | 67 | 68 | def beautify_skiptask_name(task_name): 69 | task_name = f"=> Ignoring task {task_name}" 70 | task_name = click.style(task_name, fg="cyan") 71 | click.echo("") 72 | click.echo(task_name) 73 | 74 | 75 | def beautify_task_cmd(cmd: str): 76 | """Highlighting the bash commands.""" 77 | 78 | cmd = highlight(cmd, BashLexer(), TerminalFormatter()) 79 | cmd = cmd.rstrip() 80 | click.echo(cmd) 81 | 82 | 83 | def scream(what): 84 | """Screaming 'Viewing Tasks'... or 'Running Tasks'.""" 85 | 86 | separator = "-" * 18 87 | 88 | if what == "run": 89 | click.echo() 90 | click.secho("RUNNING TASKS...", fg="green", bold=True) 91 | click.secho(separator) 92 | 93 | elif what == "view": 94 | click.echo() 95 | click.secho("VIEWING TASKS...", fg="green", bold=True) 96 | click.secho(separator) 97 | 98 | elif what == "list": 99 | click.echo() 100 | click.secho("TASK LIST...", fg="green", bold=True) 101 | click.secho(separator) 102 | 103 | elif what == "dep": 104 | click.echo() 105 | click.secho("TASK DEPENDENCIES...", fg="green", bold=True) 106 | click.secho(separator) 107 | 108 | 109 | def find_shell_path(shell_name="bash"): 110 | """Finds out system's bash interpreter path.""" 111 | 112 | if not os.name == "nt": 113 | cmd = ["which", "-a", shell_name] 114 | else: 115 | cmd = ["where", shell_name] 116 | 117 | try: 118 | c = subprocess.run( 119 | cmd, 120 | universal_newlines=True, 121 | check=True, 122 | stdout=subprocess.PIPE, 123 | stderr=subprocess.PIPE, 124 | ) 125 | output = c.stdout.split("\n") 126 | output = [_ for _ in output if _] 127 | 128 | for path in output: 129 | if path == f"/bin/{shell_name}": 130 | return path 131 | 132 | except subprocess.CalledProcessError: 133 | click.secho("Error: Bash not found. Install Bash to use Rush.", fg="magenta") 134 | sys.exit(1) 135 | 136 | 137 | def run_task(task: str, task_name: str, interactive=True, catch_errors=True): 138 | """Primary function that runs a task chunk.""" 139 | 140 | use_shell = find_shell_path() 141 | std_in = sys.stdin if interactive else subprocess.PIPE 142 | std_out = sys.stdout if interactive else subprocess.PIPE 143 | 144 | beautify_task_name(task_name) 145 | try: 146 | subprocess.run( 147 | [use_shell, "-c", task], 148 | stdin=std_in, 149 | stdout=std_out, 150 | universal_newlines=True, 151 | check=catch_errors, 152 | ) 153 | except subprocess.CalledProcessError: 154 | click.secho("Error occured: Shutting down") 155 | sys.exit(1) 156 | -------------------------------------------------------------------------------- /rushfile.yml: -------------------------------------------------------------------------------- 1 | # rushfile.yml 2 | 3 | task_1: | 4 | echo "task1 is running" 5 | 6 | task_2: | 7 | # Task chaining [task_1 is a dependency of task_2] 8 | task_1 9 | echo "task2 is running" 10 | 11 | task_3: | 12 | ls -a 13 | sudo apt-get install cowsay | head -n 0 14 | cowsay "Around the world in 80 days!" 15 | 16 | //task_4: | 17 | # Ignoring a task [task_4 will be ignored while execution] 18 | ls | grep "ce" 19 | ls > he.txt1 20 | task_5 21 | 22 | task_5: | 23 | # Running a bash script from rush 24 | ./script.sh 25 | 26 | task_6: | 27 | read -p 'Want to deploy docker container in detached mode? (y/n):' daemon 28 | if [[ $daemon=="y" ]]; then 29 | echo "Running container in detached mode." 30 | elif [[ $daemon=="n" ]]; then 31 | echo "Running container in attached mode." 32 | else 33 | echo "Running container in attached mode 2." 34 | fi 35 | 36 | task_7: | 37 | task_2 38 | task_5 39 | ls 40 | -------------------------------------------------------------------------------- /script.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo " ______ _" 4 | echo " | ___ \ | |" 5 | echo " | |_/ / _ ___| |__" 6 | echo " | / | | / __| '_ \ " 7 | echo " | |\ \ |_| \__ \ | | |" 8 | echo " \_| \_\__,_|___/_| |_|" 9 | 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednafi/rush/216d0a8f85ec90853608de4226fb61b2d287cf9f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_prep_tasks.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | import pytest 4 | import yaml 5 | from mock import patch 6 | 7 | from rush_cli.prep_tasks import PrepTasks 8 | from rush_cli.prep_tasks import Views 9 | 10 | 11 | @pytest.fixture 12 | def make_preptasks(): 13 | obj = PrepTasks() 14 | 15 | return obj 16 | 17 | 18 | def test_clean_tasks(make_preptasks): 19 | obj = make_preptasks 20 | 21 | assert obj._clean_tasks( 22 | { 23 | "task_1": 'echo "task1 is running"\n', 24 | "task_2": 'task_1\necho "task2 is running"\n', 25 | } 26 | ) == OrderedDict( 27 | [ 28 | ("task_1", ['echo "task1 is running"']), 29 | ("task_2", ["task_1", 'echo "task2 is running"']), 30 | ] 31 | ) 32 | 33 | 34 | def test_replace_placeholder_tasks(make_preptasks): 35 | obj = make_preptasks 36 | 37 | assert obj._replace_placeholder_tasks( 38 | ["task_1", 'echo "task"'], {"task_1": "hello"} 39 | ) == ["hello", 'echo "task"'] 40 | 41 | 42 | def test_flatten_task_chunk(make_preptasks): 43 | obj = make_preptasks 44 | 45 | assert obj._flatten_task_chunk( 46 | [["hello"], ["from", ["the", ["other"]], "side"]] 47 | ) == ["hello", "from", "the", "other", "side"] 48 | 49 | 50 | def test_filter_tasks(make_preptasks): 51 | obj = make_preptasks 52 | 53 | assert obj._filter_tasks( 54 | {"task_1": "ay", "task_2": "g", "task_3": "homie"}, "task_1", "task_3" 55 | ) == {"task_1": "ay", "task_3": "homie"} 56 | 57 | with pytest.raises(SystemExit): 58 | obj._filter_tasks( 59 | {"task_1": "ay", "task_2": "g", "task_3": "homie"}, "task_1", "task_4" 60 | ) 61 | 62 | 63 | @pytest.fixture(autouse=True) 64 | def make_tmpdir(tmpdir): 65 | tmp_dir = tmpdir.mkdir("folder") 66 | tmp_path = tmp_dir.join("rushfile.yml") 67 | 68 | return tmp_dir, tmp_path 69 | 70 | 71 | @pytest.fixture(autouse=True) 72 | def make_cwd(request, make_tmpdir): 73 | tmp_dir, tmp_path = make_tmpdir 74 | patched = patch("os.getcwd", return_value=tmp_dir) 75 | request.addfinalizer(lambda: patched.__exit__()) 76 | return patched.__enter__() 77 | 78 | 79 | # find_rushfile 80 | @pytest.fixture(autouse=True) 81 | def make_rushfile(make_tmpdir): 82 | """Creating dummy rushfile.yml.""" 83 | 84 | # dummy rushfile path 85 | tmp_dir, tmp_path = make_tmpdir 86 | 87 | # dummy rushfile contents 88 | content = """task_1: | 89 | echo "task1 is running" 90 | 91 | task_2: | 92 | # Task chaining [task_1 is a dependency of task_2] 93 | task_1 94 | echo "task2 is running" 95 | """ 96 | 97 | # loading dummy rushfile 98 | yml_content = yaml.load(content, Loader=yaml.FullLoader) 99 | 100 | # saving dummy rushfile to tmp dir 101 | with open(tmp_path, "w") as f: 102 | yaml.dump(yml_content, f) 103 | 104 | return yml_content 105 | 106 | 107 | @pytest.fixture 108 | def make_views(): 109 | obj = Views() 110 | return obj 111 | 112 | 113 | def test_view_rushpath(capsys, make_views): 114 | obj = make_views 115 | obj.view_rushpath 116 | captured = capsys.readouterr() 117 | print(captured.out) 118 | assert captured.out.rstrip().split("/")[-1] == "rushfile.yml" 119 | -------------------------------------------------------------------------------- /tests/test_read_tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import yaml 5 | from mock import patch 6 | 7 | from rush_cli.read_tasks import ReadTasks 8 | 9 | 10 | @pytest.fixture 11 | def make_tmpdir(tmpdir): 12 | tmp_dir = tmpdir.mkdir("data") 13 | tmp_path = tmp_dir.join("rushfile.yml") 14 | 15 | return tmp_dir, tmp_path 16 | 17 | 18 | @pytest.fixture 19 | def make_cwd(request, make_tmpdir): 20 | tmp_dir, tmp_path = make_tmpdir 21 | patched = patch("os.getcwd", return_value=tmp_dir) 22 | request.addfinalizer(lambda: patched.__exit__()) 23 | return patched.__enter__() 24 | 25 | 26 | @pytest.fixture 27 | def make_readtasks(make_cwd): 28 | """Initializing ReadTasks class.""" 29 | 30 | obj = ReadTasks( 31 | use_shell="/bin/bash", filename="rushfile.yml", current_dir=os.getcwd() 32 | ) 33 | return obj 34 | 35 | 36 | # find_rushfile 37 | @pytest.fixture 38 | def make_rushfile(make_tmpdir): 39 | """Creating dummy rushfile.yml.""" 40 | 41 | # dummy rushfile path 42 | tmp_dir, tmp_path = make_tmpdir 43 | 44 | # dummy rushfile contents 45 | content = """task_1: | 46 | echo "task1 is running" 47 | 48 | task_2: | 49 | # Task chaining [task_1 is a dependency of task_2] 50 | task_1 51 | echo "task2 is running" 52 | """ 53 | 54 | # loading dummy rushfile 55 | yml_content = yaml.load(content, Loader=yaml.FullLoader) 56 | 57 | # saving dummy rushfile to tmp dir 58 | with open(tmp_path, "w") as f: 59 | yaml.dump(yml_content, f) 60 | 61 | return yml_content 62 | 63 | 64 | def test_init(make_readtasks): 65 | obj = make_readtasks 66 | assert obj.use_shell == "/bin/bash" 67 | assert obj.filename == "rushfile.yml" 68 | 69 | 70 | # find_rushfile 71 | def test_find_rushfile(make_readtasks, make_rushfile, make_tmpdir): 72 | obj = make_readtasks 73 | tmp_dir, tmp_path = make_tmpdir 74 | assert tmp_path == obj.find_rushfile() 75 | 76 | 77 | # read_rushfile 78 | def test_read_rushfile(make_readtasks, make_rushfile): 79 | obj = make_readtasks 80 | cont = make_rushfile 81 | assert cont == obj.read_rushfile() 82 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rush_cli.utils import beautify_skiptask_name 4 | from rush_cli.utils import beautify_task_cmd 5 | from rush_cli.utils import beautify_task_name 6 | from rush_cli.utils import run_task 7 | from rush_cli.utils import scream 8 | from rush_cli.utils import walk_up 9 | 10 | 11 | @pytest.fixture() 12 | def make_tmpdir(tmpdir): 13 | tmp_dir = tmpdir.mkdir("faka") 14 | return str(tmp_dir) 15 | 16 | 17 | def test_walk_up(make_tmpdir): 18 | dirs = list(walk_up(make_tmpdir)) 19 | assert isinstance(dirs[0][0], str) 20 | assert dirs[0][0].split("/")[-1] == "faka" 21 | assert dirs[0][1] == [] 22 | assert dirs[0][2] == [] 23 | 24 | 25 | def test_beautify_task_name(capsys): 26 | 27 | beautify_task_name("task_1") 28 | captured = capsys.readouterr() 29 | assert captured.out == "\ntask_1:\n==========\n" 30 | 31 | 32 | def test_beautify_skiptask_name(capsys): 33 | beautify_skiptask_name("task_4") 34 | captured = capsys.readouterr() 35 | assert captured.out == "\n=> Ignoring task task_4\n" 36 | 37 | 38 | def test_beautify_task_cmd(capsys): 39 | beautify_task_cmd("echo 'hello'") 40 | captured = capsys.readouterr() 41 | assert captured.out == "echo 'hello'\n" 42 | 43 | 44 | def test_scream(capsys): 45 | scream("run") 46 | captured = capsys.readouterr() 47 | assert captured.out == "\nRUNNING TASKS...\n------------------\n" 48 | 49 | scream("view") 50 | captured = capsys.readouterr() 51 | assert captured.out == "\nVIEWING TASKS...\n------------------\n" 52 | 53 | scream("list") 54 | captured = capsys.readouterr() 55 | assert captured.out == "\nTASK LIST...\n------------------\n" 56 | 57 | 58 | def test_run_task(capsys, fake_process): 59 | 60 | fake_process.register_subprocess(["which", "-a", "bash"], stdout="/bin/bash") 61 | 62 | fake_process.register_subprocess( 63 | ["/bin/bash", "-c", "echo 'hello'"], stdout="echo hello" 64 | ) 65 | run_task("echo 'hello'", "task_0") 66 | captured = capsys.readouterr() 67 | assert captured.out == "\ntask_0:\n==========\n" 68 | --------------------------------------------------------------------------------