├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------