@@ -26,6 +26,7 @@ pip install python-dev-cli
2626Define custom scripts in your ` pyproject.toml ` file:
2727
2828``` toml
29+ # pyproject.toml
2930[tool .python-dev-cli .scripts ]
3031up = " docker compose up -d"
3132down = " docker compose down -v --remove-orphans"
@@ -62,6 +63,7 @@ dev --help
6263Any script that is prefixed with an underscore (` _ ` ) will be hidden from the help page and cannot be run directly:
6364
6465``` toml
66+ # pyproject.toml
6567[tool .python-dev-cli .scripts ]
6668_foo = " foo"
6769_bar = " bar"
8587You can also define scripts as a list of script references, which will be run in order:
8688
8789``` toml
90+ # pyproject.toml
8891[tool .python-dev-cli .scripts ]
8992black = " black --check --config pyproject.toml ."
9093black_fix = " black --config pyproject.toml ."
@@ -108,13 +111,13 @@ By default, scripts can utilize [Jinja2] template syntax, enabling you to refere
108111Python modules, and even other scripts:
109112
110113``` toml
114+ # pyproject.toml
111115[tool .python-dev-cli .settings ]
112- include = [" os " , " uuid:uuid4 as uuid" ]
116+ include = [" uuid:uuid4 as uuid" ]
113117
114118[tool .python-dev-cli .scripts ]
115119python = " echo {{ 1 + 1 }}"
116120module = " echo {{ uuid() }}"
117- env_vars = " echo PATH={{ os.getenv('PATH') }} PWD={{ os.getenv('PWD') }} HOME={{ os.getenv('HOME') }}"
118121other_scripts = " echo {{ dev._foo }}{{ dev._bar }}"
119122_foo = " foo"
120123_bar = " bar"
@@ -127,24 +130,24 @@ dev python
127130dev module
128131# 2b2e0b9e-0b9e-4a4a-9a9a-9a9a9a9a9a9a
129132
130- dev env_vars
131- # PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin PWD=/Users/username/Projects HOME=/Users/username
132-
133133dev other_scripts
134134# foobar
135135```
136136
137- Script template functionality can be disabled, if you prefer to keep things simple. See the [ Settings]
138- section below for more information.
137+ Script template functionality can be disabled, if you prefer to keep things simple. See the [ Settings] section below for
138+ more information.
139139
140140## Settings
141141
142- You can configure this package by adding a ` tool.python-dev-cli.settings ` section to your ` pyproject.toml ` file:
142+ You can configure this package by adding a ` tool.python-dev-cli.settings ` section to your ` pyproject.toml ` file (default
143+ values shown):
143144
144145``` toml
146+ # pyproject.toml
145147[tool .python-dev-cli .settings ]
146148enable_templates = true
147- include = [" os" , " sys" ]
149+ parse_help = true
150+ include = []
148151script_refs = " dev"
149152```
150153
@@ -159,6 +162,7 @@ and scripts will be run as-is, without any preprocessing.
159162For example, this will still work:
160163
161164``` toml
165+ # pyproject.toml
162166[tool .python-dev-cli .settings ]
163167enable_templates = false
164168
@@ -177,6 +181,7 @@ dev foobar
177181However, this will not work as intended because the template will not be parsed:
178182
179183``` toml
184+ # pyproject.toml
180185[tool .python-dev-cli .settings ]
181186enable_templates = false
182187
@@ -191,12 +196,43 @@ dev foobar
191196# {{ dev._foo }}{{ dev._bar }}
192197```
193198
199+ ### parse_help
200+
201+ Enable or disable the parsing of script templates in the ` dev ` CLI help page. If you disable this, the help page will
202+ show the raw script template, rather than the parsed script command.
203+
204+ ``` toml
205+ # pyproject.toml
206+ [tool .python-dev-cli .settings ]
207+ parse_help = false
208+
209+ [tool .python-dev-cli .scripts ]
210+ _foo = " foo"
211+ _bar = " bar"
212+ foobar = " echo {{ dev._foo }}{{ dev._bar }}"
213+ ```
214+
215+ ``` shell
216+ dev -h
217+ # usage: dev [-h] [-d] {foobar} ...
218+ # Python developer CLI for running custom scripts defined in pyproject.toml
219+ #
220+ # options:
221+ # -h, --help show this help message and exit
222+ # -d, --debug enable debug logging
223+ #
224+ # available scripts:
225+ # {foobar}
226+ # foobar ['echo {{ dev._foo }}{{ dev._bar }}']
227+ ```
228+
194229### include
195230
196231A list of modules to include in the [ Jinja2] environment, when parsing scripts. This enables you to reference Python
197232modules in your scripts:
198233
199234``` toml
235+ # pyproject.toml
200236[tool .python-dev-cli .settings ]
201237include = [" os:getcwd" , " os:getenv as env" ]
202238
@@ -229,6 +265,7 @@ Valid formats for including modules are:
229265Scripts can contain references to other scripts, using the ` {{ dev.my_script }} ` syntax:
230266
231267``` toml
268+ # pyproject.toml
232269[tool .python-dev-cli .scripts ]
233270_foo = " foo"
234271_bar = " bar"
@@ -244,6 +281,7 @@ The name of the `dev` object is configurable using the `script_refs` setting. Fo
244281` scripts ` :
245282
246283``` toml
284+ # pyproject.toml
247285[tool .python-dev-cli .settings ]
248286script_refs = " scripts"
249287
@@ -263,37 +301,34 @@ dev foobar
263301### Shell Syntax
264302
265303Scripts are run in a Python subprocess using [ subprocess.run()] , not the shell interpreter that the ` dev ` CLI is being
266- run in. As a result, it has the following limitations:
304+ run in. As a result, shell features such as pipes ` | ` ; filename wildcards ` * ` ; redirects ` > ` , ` >> ` , ` < ` ; backgrounding
305+ ` & ` ; and expansion of ` ~ ` to a user's home directory are not supported.
267306
268- - Environment variables cannot be referenced as you would in a shell script (e.g. ` $HOME ` or ` ${HOME} ` ).
269- - Shell syntax (e.g. pipes ` | ` ; redirects ` > ` , ` >> ` , ` < ` ; backgrounding [ ` & ` ] ) is not supported .
307+ > ** NOTE: ** Environment variables can still be referenced as you would in a shell script (e.g. ` $HOME ` or ` ${HOME} ` ),
308+ > because the ` dev ` CLI uses [ os.path.expandvars() ] when resolving scripts .
270309
271- To work around these limitations, you can use the ` os ` module to reference environment variables, and the ` subprocess `
272- module to run shell commands:
310+ To work around this limitation, you can use the ` subprocess ` module to run shell commands with the ` shell=True ` flag:
273311
274312``` toml
313+ # pyproject.toml
275314[tool .python-dev-cli .settings ]
276- include = [" os " , " subprocess" ]
315+ include = [" subprocess" ]
277316
278317[tool .python-dev-cli .scripts ]
279- env_vars = " echo PATH={{ os.getenv('PATH') }} PWD={{ os.getenv('PWD') }} HOME={{ os.getenv('HOME') }}"
280318shell = " echo {{ subprocess.run('echo foo | tr a-z A-Z', shell=True, capture_output=True).stdout.decode().strip() }}"
281319```
282320
283321``` shell
284- dev env_vars
285- # PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin PWD=/Users/username/Projects HOME=/Users/username
286-
287322dev shell
288323# FOO
289324```
290325
291- These constraints actually have the benefit of making your ` dev ` scripts more cross-platform compatible, as they do not
292- rely on any shell-specific syntax (unless you use the ` subprocess ` workaround, as in the example above) . This means your
293- scripts should work on Windows, Linux, and macOS.
326+ However, by not running scripts with the ` shell=True ` flag, the ` dev ` CLI has the benefit of making your scripts more
327+ cross-platform compatible, as they do not rely on any shell-specific syntax . This means your scripts will typically work
328+ on Windows, Linux, and macOS (unless you do something like in the example above) .
294329
295- Also, it's worth pointing out that the ` shell ` example above would be better written as a Python script, rather than a
296- ` dev ` script in your ` pyproject.toml ` file:
330+ Also, it's worth pointing out that the example above would be better written as a Python script, rather than a ` dev `
331+ script in your ` pyproject.toml ` file:
297332
298333``` python
299334# scripts/shell.py
@@ -310,13 +345,18 @@ if __name__ == '__main__':
310345Then, you could reference it in your script definition:
311346
312347``` toml
348+ # pyproject.toml
313349[tool .python-dev-cli .scripts ]
314350shell = " python -m scripts.shell"
315351```
316352
317353This is a better design pattern, as it keeps your Python logic in a Python file rather than a TOML file, where it cannot
318354be easily tested or linted.
319355
356+ An even better approach would be to use the various Python implementations of shell-like features (e.g. [ glob] ,
357+ [ fnmatch] , [ os.walk()] , [ os.path.expanduser()] , and [ shutil] ) to achieve the desired result, rather than calling
358+ out to a shell command.
359+
320360### Script Names
321361
322362Script names must be valid Python identifiers, which means they must start with a letter or underscore (` _ ` ), and can
@@ -326,6 +366,131 @@ only contain letters, numbers, and underscores (`_`). This is because script na
326366Also, keep in mind that any scripts prefixed with an underscore (` _ ` ) will be hidden from the help page and cannot be
327367run directly. Think of these as "private" script variables, which can only be referenced by other scripts.
328368
369+ ### Template Parsing
370+
371+ By default, script templates are parsed using [ Jinja2] before being run. They are also parsed whenever the ` dev ` CLI
372+ help page is displayed, to generate examples of the actual commands that will be run for each script. This means that
373+ any function calls or other Python syntax in your scripts will be evaluated ** even when a script is not being run**
374+ (potentially multiple times, depending on your configuration). This is important to understand, because it could have
375+ unintended consequences.
376+
377+ For example, let's say you have one script that generates a random UUID and another script that runs the first script
378+ three times:
379+
380+ ``` toml
381+ # pyproject.toml
382+ [tool .python-dev-cli .settings ]
383+ include = [" uuid:uuid4 as uuid" ]
384+
385+ [tool .python-dev-cli .scripts ]
386+ uuid = " echo {{ uuid() }}"
387+ uuids = [" uuid" , " uuid" , " uuid" ]
388+ ```
389+
390+ When you run ` dev uuids ` , the ` uuid() ` function gets called three times, generating three unique IDs as expected.
391+
392+ ``` shell
393+ dev uuids
394+ # affa5cf5-8d1d-43c6-806f-c561d91f7d05
395+ # d8dd276d-1cc8-4dcf-9e0d-6f3fcbdbcc22
396+ # ac5a0a59-4510-4609-9734-7c238652f59a
397+ ```
398+
399+ When you run ` dev --help ` , the same thing happens, this time to generate the help page examples:
400+
401+ ``` shell
402+ dev -h
403+ # usage: dev [-h] [-d] {foobar} ...
404+ # Python developer CLI for running custom scripts defined in pyproject.toml
405+ #
406+ # options:
407+ # -h, --help show this help message and exit
408+ # -d, --debug enable debug logging
409+ #
410+ # available scripts:
411+ # {uuid,uuids}
412+ # uuid ['echo 2afa5b31-159c-45e5-b7e3-ad935a48cef0']
413+ # uuids ['echo 2c7fe6e2-01a0-4cd9-90fe-50816a2ed316', 'echo 51ca85c8-b69b-4151-ab3f-5bf2a148a058', 'echo 6595e224-3e24-463c-9cc4-98c9bf0ecddb']
414+ ```
415+
416+ Now, consider the following scripts that make API calls to a third-party service:
417+
418+ ``` toml
419+ # pyproject.toml
420+ [tool .python-dev-cli .settings ]
421+ include = [" json" , " requests" ]
422+
423+ [tool .python-dev-cli .scripts ]
424+ curl = " curl https://cat-fact.herokuapp.com/facts/random?amount=1"
425+ req = " echo {{ json.loads(requests.get('https://cat-fact.herokuapp.com/facts/random?amount=1').content).text }}"
426+ ```
427+
428+ When you run ` dev curl ` , the API call is made as expected:
429+
430+ ``` shell
431+ dev curl
432+ # {"status":{"verified":true,"sentCount":1},"_id":"591f98783b90f7150a19c1c5","__v":0,"text":"Baking chocolate is the most dangerous chocolate to your cat.","source":"api","updatedAt":"2020-08-23T20:20:01.611Z","type":"cat","createdAt":"2018-04-17T20:20:02.627Z","deleted":false,"used":false,"user":"5a9ac18c7478810ea6c06381"}
433+ ```
434+
435+ When you run ` dev req ` , the API call is also made as expected:
436+
437+ ``` shell
438+ dev req
439+ # Cats are amazing animals.
440+ ```
441+
442+ When you run ` dev --help ` , the API call is made while parsing the ` req ` script template, even though the script is not
443+ being run:
444+
445+ ``` shell
446+ dev --help
447+ # usage: dev [-h] [-d] {curl,req} ...
448+ # Python developer CLI for running custom scripts defined in pyproject.toml
449+ #
450+ # options:
451+ # -h, --help show this help message and exit
452+ # -d, --debug enable debug logging
453+ #
454+ # available scripts:
455+ # {curl,req}
456+ # curl ['curl https://cat-fact.herokuapp.com/facts/random?amount=1']
457+ # req ['echo When asked if her husband had any hobbies, Mary Todd Lincoln is said to have replied cats.']
458+ ```
459+
460+ If that were an API call that had a side effect, such as creating a new record in a database, then that side effect
461+ would happen every time the ` dev --help ` command was run. This is probably not what you want.
462+
463+ To prevent this from happening, you can disable template parsing in the ` dev ` CLI help page by setting the ` parse_help `
464+ value to ` false ` :
465+
466+ ``` toml
467+ # pyproject.toml
468+ [tool .python-dev-cli .settings ]
469+ parse_help = false
470+ ```
471+
472+ However, a better solution in most cases would be to use the ` curl ` command, or move the API call to a Python script
473+ that can be run using the ` python -m ` syntax:
474+
475+ ``` toml
476+ # pyproject.toml
477+ [tool .python-dev-cli .scripts ]
478+ req = " python -m scripts.req"
479+ ```
480+
481+ ``` python
482+ # scripts/req.py
483+ import json
484+ import requests
485+
486+ def main () -> str :
487+ return json.loads(requests.get(' https://cat-fact.herokuapp.com/facts/random?amount=1' ).content).text
488+
489+ if __name__ == ' __main__' :
490+ output: str = main()
491+ print (output)
492+ ```
493+
329494## License
330495
331496This open source project is licensed under the terms of the [ BSD 3-Clause License] .
@@ -344,7 +509,13 @@ page.
344509[ Code of Conduct ] : https://github.com/sscovil/devblob/master/CODE_OF_CONDUCT.md
345510[ CONTRIBUTING.md ] : https://github.com/sscovil/devblob/master/CONTRIBUTING.md
346511[ Contributor Covenant ] : https://contributor-covenant.org/
512+ [ fnmatch ] : https://docs.python.org/3/library/fnmatch.html#module-fnmatch
513+ [ glob ] : https://docs.python.org/3/library/glob.html#module-glob
347514[ Jinja2 ] : https://jinja.palletsprojects.com/en/3.0.x/
515+ [ os.path.expanduser() ] : https://docs.python.org/3/library/os.path.html#os.path.expanduser
516+ [ os.path.expandvars() ] : https://docs.python.org/3/library/os.path.html#os.path.expandvars
517+ [ os.walk() ] : https://docs.python.org/3/library/os.html#os.walk
348518[ pyproject.toml ] : https://peps.python.org/pep-0518/#tool-table
349519[ Settings ] : https://github.com/sscovil/python-dev-cli/blob/main/README.md#settings
520+ [ shutil ] : https://docs.python.org/3/library/shutil.html#module-shutil
350521[ subprocess.run() ] : https://docs.python.org/3/library/subprocess.html#subprocess.run
0 commit comments