This commit is contained in:
2019-09-14 18:01:23 -04:00
parent 73c8559f47
commit 269e36d6e2
9 changed files with 464 additions and 0 deletions
+1
View File
@@ -1 +1,2 @@
.vscode/settings.json
bashf.egg-info/*
+4
View File
@@ -0,0 +1,4 @@
test: hello
echo 'bye'
hello:
echo 'hi'
+16
View File
@@ -0,0 +1,16 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
black = "*"
[packages]
bashf = {editable = true,path = "."}
[requires]
python_version = "3.7"
[pipenv]
allow_prereleases = true
Generated
+111
View File
@@ -0,0 +1,111 @@
{
"_meta": {
"hash": {
"sha256": "586d266e0093a64d666ab313fd620746db0d04d1dc6da5a184ffc6e3bf2ee04c"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"bash.py": {
"hashes": [
"sha256:108279834ec18c6597806393c3779076b07013c1cdbc3ffd9197dc04985ac8ba",
"sha256:be5a86449866642adea843c08b0075990f36294ad42681eb41e9924307e3b4af"
],
"version": "==0.4.2"
},
"bashf": {
"editable": true,
"path": "."
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"colorama": {
"hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"version": "==0.4.1"
},
"crayons": {
"hashes": [
"sha256:41f0843815a8e3ac6fb445b7970d8b9c766e6f164092d84e7ea809b4c91418ec",
"sha256:8edcadb7f197e25f2cc094aec5bf7f1b6001d3f76c82d56f8d46f6fb1405554f"
],
"version": "==0.2.0"
},
"delegator.py": {
"hashes": [
"sha256:814657d96b98a244c479e3d5f6e9e850ac333e85f807d6bc846e72bbb2537806",
"sha256:e6cc9cedab9ae59b169ee0422e17231adedadb144e63c0b5a60e6ff8adf8521b"
],
"version": "==0.1.1"
},
"pexpect": {
"hashes": [
"sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1",
"sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"
],
"version": "==4.7.0"
},
"ptyprocess": {
"hashes": [
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
],
"version": "==0.6.0"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"attrs": {
"hashes": [
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
],
"version": "==19.1.0"
},
"black": {
"hashes": [
"sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
"sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
],
"index": "pypi",
"version": "==19.3b0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
}
}
}
+1
View File
@@ -1,3 +1,4 @@
# Bashfile: Like Make + Bash, Combined.
I love using `Makefile`s for one-off tasks in projects. The problem with doing this, is you can't use familiar bashisms when doing so, as GNU Make doesn't use the familiar Bash sytnax. This project seeks to bridge these works.
View File
+114
View File
@@ -0,0 +1,114 @@
import os
import json
import bash
class NoBashfileFound(RuntimeError):
pass
class TaskNotInBashfile(ValueError):
pass
class TaskScript:
def __init__(self, *, bashfile, name):
self.bashfile = bashfile
self.name = name
def __repr__(self):
return f"<TaskScript name={self.name!r} depends_on={self.depends_on!r} src={self.bashfile.path!r}>"
@property
def declaration_line(self):
for line in self.bashfile.source_lines:
if line.startswith(self.name):
return line
@property
def depends_on(self):
# TODO: return script objects.
task_names = self.declaration_line.split(':')[1].split()
return [TaskScript(bashfile=self.bashfile, name=n) for n in task_names]
@classmethod
def from_declaration_line(Class, s, *, bashfile):
name = s.split(':')[0].strip()
return Class(bashfile=bashfile, name=name)
def run(self):
bash.run(self.task_script)
class Bashfile:
def __init__(self, *, path):
self.path = path
self.environ = {}
if not os.path.exists(path):
raise NoBashfileFound()
def add_environ(self, key, value):
self.environ[key] = value
def add_environ_json(self, s):
try:
j = json.loads(s)
except json.JSONDecodeError:
assert os.path.exists(s)
# Assume a path was passed, instead.
with open(s, 'r') as f:
j = json.load(f)
self.environ.update(j)
@property
def home_path(self):
return os.path.abspath(os.path.dirname(self.path))
@classmethod
def find(
Class,
*,
filename='Bashfile',
root=os.getcwd(),
max_depth=4,
topdown=True,
):
"""Returns the path of a Pipfile in parent directories."""
i = 0
for c, d, f in os.walk(root, topdown=topdown):
if i > max_depth:
raise NoBashfileFound('No {filename} found!')
elif filename in f:
return Class(path=os.path.join(c, filename))
i += 1
@property
def source_text(self):
with open(self.path, 'r') as f:
return f.read()
@property
def source_lines(self):
return self.source_text.split('\n')
@staticmethod
def _is_declaration_line(line):
return not (line.startswith(' ') or line.startswith('\t'))
@property
def scripts(self):
def iter_task_lines():
for line in self.source_lines:
if line:
if self._is_declaration_line(line):
yield line.rstrip()
scripts = {}
for line in iter_task_lines():
if self._is_declaration_line(line):
script = TaskScript.from_declaration_line(line, bashfile=self)
scripts[script.name] = script
return scripts
+89
View File
@@ -0,0 +1,89 @@
import click
import crayons
from .bashfile import Bashfile
@click.command()
@click.argument(
'task',
type=click.STRING,
default='__LIST_ALL__',
envvar="BASHFILE_TASK",
# required=False,
)
@click.option(
'--bashfile',
'-b',
default='__BASHFILE__',
envvar='BASHFILE_PATH',
nargs=1,
type=click.Path(),
)
@click.option('--list', '-l', '_list', default=False, is_flag=True)
@click.option('--debug', default=False, is_flag=True, hidden=True)
@click.option('--shellcheck', default=False, is_flag=True, hidden=True)
@click.option(
'--environ',
'-e',
nargs=2,
type=click.STRING,
multiple=True,
help='task environment variable (can be passed multiple times).',
)
@click.option(
'--arg',
'-a',
nargs=1,
type=click.STRING,
multiple=True,
help='task ARGV arguments (can be passed multiple times).',
)
@click.option(
'--environ-json',
'-j',
nargs=1,
type=click.STRING,
help='environment variables, in JSON format.',
)
def task(
*, task, bashfile, arg, _list, environ, environ_json, shellcheck, debug
):
"""bashf — Bashfile runner (the familiar Bash/Make hybrid)."""
# Default to list behavior, when no task is provided.
if task == '__LIST_ALL__':
_list = True
task = None
if bashfile == '__BASHFILE__':
bashfile = Bashfile.find(root='.')
if environ_json:
bashfile.add_environ_json(environ_json)
for env in environ:
key, value = env[:]
if debug:
click.echo(
f" Setting environ: {crayons.red(key)} {crayons.white('=')} {value}.",
err=True,
)
bashfile.add_environ(key, value)
print(bashfile.scripts)
# print(locals())
def entrypoint():
try:
main()
except KeyboardInterrupt:
print('ool beans.')
def main():
task()
if __name__ == '__main__':
entrypoint()
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Note: To use the 'upload' functionality of this file, you must:
# $ pipenv install twine --dev
import io
import os
import sys
from shutil import rmtree
from setuptools import find_packages, setup, Command
# Package meta-data.
NAME = 'bashf'
DESCRIPTION = 'The familar Make / Bash hybrid.'
URL = 'https://github.com/kennethreitz/bashfile'
EMAIL = 'me@kennethreitz.org'
AUTHOR = 'Kenneth Reitz'
REQUIRES_PYTHON = '>=3.6.0'
VERSION = '0.1.0'
# What packages are required for this module to be executed?
REQUIRED = ['click', 'delegator.py', 'crayons', 'bash.py']
# What packages are optional?
EXTRAS = {
# 'fancy feature': ['django'],
}
# The rest you shouldn't have to touch too much :)
# ------------------------------------------------
# Except, perhaps the License and Trove Classifiers!
# If you do change the License, remember to change the Trove Classifier for that!
here = os.path.abspath(os.path.dirname(__file__))
# Import the README and use it as the long-description.
# Note: this will only work if 'README.md' is present in your MANIFEST.in file!
try:
with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
long_description = '\n' + f.read()
except FileNotFoundError:
long_description = DESCRIPTION
# Load the package's __version__.py module as a dictionary.
about = {}
if not VERSION:
project_slug = NAME.lower().replace("-", "_").replace(" ", "_")
with open(os.path.join(here, project_slug, '__version__.py')) as f:
exec(f.read(), about)
else:
about['__version__'] = VERSION
class UploadCommand(Command):
"""Support setup.py upload."""
description = 'Build and publish the package.'
user_options = []
@staticmethod
def status(s):
"""Prints things in bold."""
print('\033[1m{0}\033[0m'.format(s))
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
self.status('Removing previous builds…')
rmtree(os.path.join(here, 'dist'))
except OSError:
pass
self.status('Building Source and Wheel (universal) distribution…')
os.system(
'{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)
)
self.status('Uploading the package to PyPI via Twine…')
os.system('twine upload dist/*')
self.status('Pushing git tags…')
os.system('git tag v{0}'.format(about['__version__']))
os.system('git push --tags')
sys.exit()
# Where the magic happens:
setup(
name=NAME,
version=about['__version__'],
description=DESCRIPTION,
long_description=long_description,
long_description_content_type='text/markdown',
author=AUTHOR,
author_email=EMAIL,
python_requires=REQUIRES_PYTHON,
url=URL,
packages=find_packages(
exclude=["tests", "*.tests", "*.tests.*", "tests.*"]
),
# If your package is a single module, use this instead of 'packages':
# py_modules=['mypackage'],
entry_points={'console_scripts': ['bashf=bashf.cli:entrypoint']},
install_requires=REQUIRED,
extras_require=EXTRAS,
include_package_data=True,
license='MIT',
classifiers=[
# Trove classifiers
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
],
# $ setup.py publish support.
cmdclass={'upload': UploadCommand},
)