├── .backstage
└── database.json
├── .gitignore
├── .pyrustic
└── buildver
│ └── build_report
├── LICENSE
├── MANIFEST.in
├── README.md
├── VERSION
├── backstage.tasks
├── backstage
├── __init__.py
├── __main__.py
├── cli
│ └── __init__.py
├── constant
│ ├── .private
│ └── __init__.py
├── core
│ ├── .private
│ └── __init__.py
├── error
│ └── __init__.py
├── namespace
│ ├── .private
│ └── __init__.py
├── pattern
│ ├── .private
│ └── __init__.py
├── runner
│ ├── .private
│ └── __init__.py
├── text
│ ├── .private
│ └── __init__.py
├── usage
│ ├── .private
│ └── __init__.py
└── util
│ ├── .private
│ └── __init__.py
├── docs
└── modules
│ ├── README.md
│ └── content
│ ├── backstage.cli
│ ├── README.md
│ └── content
│ │ ├── classes
│ │ └── Cli.md
│ │ └── functions.md
│ ├── backstage.error
│ ├── README.md
│ └── content
│ │ ├── classes
│ │ ├── Break.md
│ │ ├── Continue.md
│ │ ├── Error.md
│ │ ├── Exit.md
│ │ ├── Fail.md
│ │ ├── FailedAssertion.md
│ │ ├── IndentError.md
│ │ ├── InterpretationError.md
│ │ ├── Return.md
│ │ ├── SubprocessError.md
│ │ └── VariableError.md
│ │ └── functions.md
│ └── backstage
│ ├── README.md
│ └── content
│ ├── classes
│ └── Backstage.md
│ └── functions.md
├── pyproject.toml
├── setup.cfg
├── setup.py
└── tests
├── __init__.py
└── __main__.py
/.backstage/database.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # File generated by Setupinit
2 |
3 | # Python
4 | __pycache__/
5 | *.py[cod]
6 | build/
7 | dist/
8 | *.egg-info/
9 |
10 | # Environments
11 | venv/
12 | env/
13 |
14 | # PyCharm
15 | .idea/
16 |
17 | # Databases
18 | *.db
19 |
20 | # Pyrustic local data
21 | .pyrustic/
22 |
23 | # Backstage local data
24 | .backstage/
25 |
--------------------------------------------------------------------------------
/.pyrustic/buildver/build_report:
--------------------------------------------------------------------------------
1 | 0.0.22 1686305391
2 | 0.0.21 1684428727
3 | 0.0.20 1677288961
4 | 0.0.19 1663938957
5 | 0.0.18 1663938876
6 | 0.0.17 1663365719
7 | 0.0.16 1663360194
8 | 0.0.15 1663358090
9 | 0.0.14 1663356741
10 | 0.0.13 1663346473
11 | 0.0.12 1663341908
12 | 0.0.11 1663284563
13 | 0.0.10 1662592046
14 | 0.0.9 1647894236
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021, 2022, 2023 Pyrustic Evangelist
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 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include backstage *
2 | global-exclude *.py[cod]
3 | include VERSION
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 | # Pyrustic Backstage
13 | Three-speed scripting language and task automation tool
14 |
15 | This project is part of the [Pyrustic Open Ecosystem](https://pyrustic.github.io).
16 | > [Installation](#installation) [Demo](#demo) [Latest](https://github.com/pyrustic/backstage/tags) [Modules](https://github.com/pyrustic/backstage/tree/master/docs/modules#readme)
17 |
18 |
19 | ## Table of contents
20 | - [Overview](#overview)
21 | - [Structure of the script file](#structure-of-the-script-file)
22 | - [Spawning processes and branching subtasks](#spawning-processes-and-branching-subtasks)
23 | - [Data types and control flow](#data-types-and-control-flow)
24 | - [Namespaces and persistence](#namespaces-and-persistence)
25 | - [Variable interpolation and escaping](#variable-interpolation-and-escaping)
26 | - [Environment variables and language syntax](#environment-variables-and-language-syntax)
27 | - [File and directory manipulation](#file-and-directory-manipulation)
28 | - [Exception handling and tests](#exception-handling-and-tests)
29 | - [Interfacing with Python](#interfacing-with-python)
30 | - [Exception handling and tests](#exception-handling-and-tests)
31 | - [Command line interface and developer experience](#command-line-interface-and-developer-experience)
32 | - [Miscellaneous](#miscellaneous)
33 | - [Demo](#demo)
34 | - [Installation](#installation)
35 |
36 | # Overview
37 | **Backstage** is a cross-platform **automation tool** that looks for a `backstage.tasks` file in the current working directory to run a specific task defined in that file on demand. A task can be a sequence or pipeline of processes to be spawned, instructions for performing file and directory manipulation, or something more sophisticated.
38 |
39 | The `backstage.tasks` file uses the [Jesth](https://github.com/pyrustic/jesth) (**J**ust **E**xtract **S**ections **T**hen **H**ack) file format that acts like a broken [INI file](https://en.wikipedia.org/wiki/INI_file) parser that only extract sections each made of a header and a body which is just a list of lines.
40 |
41 | Using an eponymous **three-speed scripting language** designed for the automation tool, the programmer can, inside the `backstage.tasks` file, define, coordinate and use the various resources at his disposal to automate things.
42 |
43 | The three-speed scripting language concept is inspired from the three forward gear ratios of early automobiles [transmission system](https://en.wikipedia.org/wiki/Manual_transmission). In the following subsections, we will explore each metaphorical gear of the **Backstage** scripting language, then we will briefly expose the automation tool itself.
44 |
45 | ## First gear
46 | In first gear, a `backstage.tasks` file is intended to store a list of tasks related to a specific project, each task exposing a list of commands or subtasks to be executed. Here, a command represents a process or pipeline of processes to be spawned. [Environment variables](#environment-variables-and-language-syntax) can be used in commands via [variable interpolation](https://en.wikipedia.org/wiki/String_interpolation). No other logic is involved.
47 |
48 | ### Example
49 | ```
50 | [task1]
51 | # three commands to run sequentially
52 | $ git commit -m 'Update'
53 | $ python -m my.package.module
54 | $ program1 arg1 | program2 {HOME}
55 | ---
56 | # run the subtask 'task2'
57 | & task2
58 | ---
59 | # run the subtask 'task3' in a new thread
60 | ~ task3
61 |
62 | [task2]
63 | $ program val1 {CWD}
64 | $ git push origin master
65 |
66 | [task3]
67 | # some heavy computation
68 | $ engine -x 5000
69 | $ engine --cleanup
70 |
71 | [_task4]
72 | # This is a private task (with an underscore as prefix)
73 | $ clean dir
74 | ```
75 |
76 | ## Second gear
77 | In second gear, the `backstage.tasks` file not only stores the tasks like in first gear, but here logic intervenes, variables are defined, control flow is used, built-in commands are called, et cetera. Basically, in second gear, **Backstage** unleashes its power and allows the programmer to anticipate problems, make sophisticated combinations of subtasks, in short to write a real **script to automate things**.
78 |
79 | ### Example
80 | ```
81 | [task1]
82 | # commit changes
83 | $ git commit -m 'Update'
84 | ---
85 | # tell user if 'Commit' has been success
86 | if R == 0
87 | # print 'Success !'
88 | : Success !
89 | else
90 | : Failed to commit changes
91 | ---
92 | # say hello ten times
93 | set age = 42
94 | set name = `John Doe`
95 | from 1 to 10
96 | $ python -m say.hello {name} {age}
97 | ---
98 | # create a file in user home
99 | set pathname = {HOME}/iliad.txt
100 | create file pathname
101 | ---
102 | # append some data to a file
103 | set data = `\nHello World !`
104 | append data to pathname
105 | ---
106 | # browse current working directory
107 | browse files and dirs in CWD
108 | : Directory -> {R}
109 | : Files ->
110 | for item in files
111 | : - {item}
112 | : Dirs ->
113 | for item in dirs
114 | : - {item}
115 | ```
116 |
117 | ## Third gear
118 | In third gear, in addition with whatever can be done with previous gears, the programmer can directly from the `backstage.tasks` file, call [Python](https://www.python.org/about/) functions with arguments and get the return ! Thanks to this third gear, any too complex or overly verbose calculation can be written in **Python** and called from **Backstage**. This [functionality](#interfacing-with-python) alone proves that **Backstage** is all about making the programmer's life better, not pretending to replace existing mature solutions that actually work.
119 |
120 | ### Example
121 | ```
122 | [task1]
123 | interface with package.coffeemaker alias coffeebro
124 |
125 | set sugar_cubes (int) = 1 + 1 + 1
126 | set extra = `milk`
127 |
128 | call coffeebro.make(sugar_cubes, extra, 42)
129 |
130 | if R == 1
131 | : Coffee successfully made !
132 | else
133 | : Oops, failed to make coffee...
134 | : Exception -> {EXCEPTION}
135 | : Traceback -> {TRACEBACK}
136 | ```
137 |
138 | ## Automation tool
139 | The scripting language help to define tasks in the `backstage.tasks` file that is intended to be consumed by the automation tool. As an automation tool, **Backstage** exposes a [command line interface](#command-line-interface-and-developer-experience) that allows the user to **discover** available tasks, **run** a task with arguments, read a task [documentation](#embedded-documentation-and-tests), use a [glob](https://en.wikipedia.org/wiki/Glob_(programming))-like syntax to **search** for a task by its name or by a keyword (case-insensitive mode) that is part of the documentation of the task, et cetera.
140 |
141 | ### Example 1
142 | Let's assume that there is a `backstage.tasks` file in the directory `/home/alex/project`. This `backstage.tasks` file contains three public tasks and one private tasks (prefixed with an underscore).
143 |
144 | This example shows how one could use the automation tool to run a task defined in the `backstage.tasks` file:
145 |
146 | ```bash
147 | $ cd /home/alex/project
148 |
149 | $ backstage -c
150 | Available tasks (3):
151 | make_coffee task1 task2
152 |
153 | $ backstage make_coffee
154 | Making coffee...
155 |
156 | $ backstage mak*
157 | Making coffee...
158 |
159 | $ backstage make_coffee sugar=3
160 | Making coffee with 3 sugar cubes...
161 | ```
162 |
163 | ### Example 2
164 | This is the contents of a `backstage.tasks` file located at `/home/alex/project`:
165 |
166 | ```
167 | [task]
168 | # define 'x' as a variable with an 'int' assignment tag
169 | set x (int) = 1 + 1 + 1
170 |
171 | # default name (by default, the assignment tag is 'str')
172 | set default_name = `John Doe`
173 |
174 | # print some text
175 | : Hi and Welcome !
176 | : I can print your name {x} times in a row !
177 |
178 | # take user input (always a 'str')
179 | > name : `What is your name ? `
180 |
181 | # control flow
182 | if name == EMPTY
183 | set name = {default_name}
184 |
185 | # iteration
186 | from 1 to x
187 | : {R} - Ave {name} !
188 |
189 | # branch subtask '_task2' and pass it an argument
190 | & _task2 {name}
191 |
192 |
193 | [_task2]
194 | # define the variable 'name' (first argument passed to this task)
195 | set name = {ARGS[0]}
196 |
197 | # Just say Goodbye !
198 | : Goodbye {name} !
199 |
200 | ```
201 |
202 | Let's run the task named `task` from the command line:
203 |
204 | ```bash
205 | $ cd /home/alex/project
206 |
207 | $ backstage task
208 | Hi and Welcome !
209 | I can print your name 3 times in a row !
210 | What is your name ? Alex
211 | 1 - Ave Alex !
212 | 2 - Ave Alex !
213 | 3 - Ave Alex !
214 | Goodbye Alex !
215 | ```
216 |
217 | > **Note:** You can reproduce this example as it. Just [install](#installation) **Backstage**, copy-paste the script in a `backstage.tasks` file, then run `backstage task` in the command line.
218 |
219 | ## And more
220 | There is more to talk about **Backstage**, like the ability to embed documentation and tests in the `backstage.tasks` file and access them from the command line. Backstage is enough versatile to do the job of a trivial task runner or to automate things with its scripting language that has a built-in bridge to the powerful Python ecosystem.
221 |
222 | In the following sections, we will explore this project in depth. You can also jump to the [demo](#demo) to start playing with **Backstage** !
223 |
224 | # Structure of the script file
225 | As stated in the [Overview](#overview) section, a `backstage.tasks` file is basically a [JesthFile](https://github.com/pyrustic/jesth).
226 |
227 | In a `backstage.tasks` file, a section represents a task. The section title is the name of the task and the section body is made of commands to run and the constructs of the **Backstage** scripting language. A valid task name is an alphanumeric string that can contains an underscore.
228 |
229 | ```
230 | You can write here at the top of the script,
231 | a description of the script,
232 | the date of its creation,
233 | or any useful information.
234 |
235 | [task1]
236 | # body of task1
237 | ...
238 |
239 | [task2]
240 | # body of task2
241 | ...
242 |
243 | [_private]
244 | # prefix a task name with an underscore
245 | # to turn it into a private task that
246 | # won't appear in the list of available tasks
247 | # when you will type 'backstage --check' in the command line
248 | ```
249 |
250 | As you can guess, a line starting with `#` is a comment. But this is only true inside a task body, because in fact not all sections are tasks as described before: a section can also be an embedded test or documentation.
251 |
252 |
253 | ## Embedded documentation
254 | You can embed documentation inside the `backstage.tasks` file. To create a documentation for a task, create a section which name is postfixed with `.help`:
255 |
256 | ```
257 | [task1]
258 | pass
259 |
260 |
261 | [task1.help]
262 | This is the description line.
263 |
264 | Usage:
265 | backstage task
266 |
267 | Options:
268 | -m, --msg Show message
269 | -x, --exit Exit blah blah
270 |
271 | ```
272 |
273 | From the **command line**, you can read the documentation of an arbitrary task:
274 |
275 | ```bash
276 | $ backstage -h task1
277 | This is the description line.
278 |
279 | Usage:
280 | backstage task
281 |
282 | Options:
283 | -m, --msg Show message
284 | -x, --exit Exit blah blah
285 | ```
286 |
287 | ## Embedded tests
288 |
289 | You can embed tests inside the `backstage.tasks` file. To create a test for a task, create a section which name is postfixed with `.test`:
290 |
291 | ```
292 | [task1.test]
293 | # perform some test here
294 |
295 | # ...
296 |
297 | assert some_var == some_var
298 | ```
299 |
300 | From the **command line**, you can run the test of an arbitrary task:
301 |
302 | ```bash
303 | $ backstage --test task1
304 | ```
305 |
306 | or the tests of a bunch of tasks:
307 |
308 | ```bash
309 | $ backstage --test task1 task2 task3
310 | ```
311 |
312 | or run all tests defined in the `backstage.tasks` file:
313 |
314 | ```bash
315 | $ backstage --test
316 | ```
317 |
318 |
319 | # Spawning processes and branching subtasks
320 | You can write commands to spawn a process or a pipeline of processes:
321 |
322 | ```
323 | [task]
324 | # spawn Git to perform a 'commit'
325 | $ git commit -m "Update"
326 |
327 | # spawn a pipeline of three processes
328 | $ program1 arg1 | program2 arg2 | program3
329 | ```
330 |
331 | Commands to spawn processes support variable interpolation:
332 | ```
333 | [task]
334 | # use HOME environment variable
335 | $ ls {HOME}
336 |
337 | # access the first index of the ARGS list
338 | $ program {ARGS[0]}
339 | ```
340 |
341 | You can **push** data to the **input** of a process:
342 |
343 | ```
344 | [task]
345 | # define variables name and age
346 | set name = `John Doe`
347 | set age (int) = 40 + 2
348 |
349 | # name and age will be pushed to the input of the next spawned process
350 | push name age
351 | $ program1 {HOME}
352 |
353 | # from now you can access via the environment variable R,
354 | # the return code (exit status code)
355 | : The return code is {R}
356 | ```
357 |
358 |
359 | You can **capture** a process:
360 |
361 | ```
362 | [task]
363 | # you can capture a process,
364 | # so you will get a direct access to the output and error
365 | ($) program2
366 |
367 | # print the content of OUTPUT and ERROR
368 | : Output -> {OUTPUT}
369 | : Error -> {ERROR}
370 | ```
371 |
372 |
373 | You can redirect **STDOUT** and **STDERR**:
374 |
375 | ```
376 | [task]
377 |
378 | # redirect STDOUT and STDERR
379 | set STDOUT = `/path/to/file_out`
380 | set STDERR = `/path/to/file_err`
381 |
382 | $ program1
383 |
384 | ---
385 |
386 | # cross platform DEVNULL:
387 | set STDOUT = /dev/null
388 |
389 | $ program2
390 | ```
391 |
392 | ## Branching subtasks
393 | You can branch a subtask and pass arguments to it:
394 |
395 | ```
396 | [task1]
397 | # branch 'task2'
398 | & task2 {HOME}
399 |
400 | [task2]
401 | # from task2, we can access arguments passed to it.
402 | # ARGS (environment variable) is a list of arguments.
403 | : Arguments -> {ARGS}
404 | ```
405 |
406 | Whenever you branch a subtask, a new instance of this subtask is created, with its own variables. You can share data with a subtask with one of these three ways:
407 | - pass arguments to the subtask while branching it;
408 | - use the global [namespace](#namespaces-and-persistence);
409 | - use the database [namespace](#namespaces-and-persistence).
410 |
411 | A subtask can also return data that is cached in the `R` environment variable:
412 |
413 | ```
414 | [task1]
415 | & task2 "John Doe"
416 | # from now, R contains 'Hello John Doe !'
417 |
418 | [task2]
419 | set result = `Hello {ARGS} !`
420 | return result
421 | ```
422 |
423 | ### Multithreading
424 | Branching a subtask is done in the main thread. But one can create a new thread for a subtask:
425 |
426 | ```
427 | [task1]
428 | # run an instance of task2 in a new thread
429 | ~ task2
430 |
431 | # the next command won't wait 'task2' to complete
432 | $ git commit -m "Update"
433 |
434 |
435 | [task2]
436 | # this task sleeps for 5 seconds
437 | sleep 5
438 | ```
439 |
440 |
441 | # Data types and control flow
442 | In the next subsections we will talk about data types then control flow.
443 |
444 | ## Data types
445 | **Backstage** supports [variables](https://en.wikipedia.org/wiki/Variable_(computer_science)) and let the programmer set, use, clear, and drop variables.
446 |
447 | Under the hood, **Backstage** works with five **Python** data types: `str`, `list` (one-dimensional), `dict` (one-dimensional), `int`, and `float`. But these data types aren't intended to be directly used by the programmer. Instead, `assignment tags` are used to tell the interpreter how to treat a variable.
448 |
449 | These `assignment tags` are: `raw`, `str`, `list`, `dict`, `int`, `float`, `date`, `time`, `dtime`, and `tstamp`.
450 |
451 | ```
452 | [task]
453 | # this is a string
454 | set var = 42
455 |
456 | # this is another string
457 | set var (str) = `Hello World`
458 |
459 | # this is a list
460 | set var (list) = `reg green blue`
461 |
462 | # this is the same list but edited
463 | set var[0] = `yellow`
464 | # var -> yellow green blue
465 |
466 | # this is a dict
467 | set var (dict) = `user="John Doe" age=42 location=Kernel`
468 |
469 | # this is the same dict but edited
470 | set var.user = `Jane Doe`
471 | # var -> user='Jane Doe' age=42 location=Kernel
472 |
473 | # this is an integer
474 | set var (int) = 40 + 2
475 | # var -> 42
476 |
477 | # this is a raw string
478 | set regex (raw) = `[\S\s]*?`
479 |
480 | # get the current timestamp
481 | set now (int) = {NOW}
482 | # now -> 1662569326
483 |
484 | # convert it into datetime
485 | set var (dtime) = {now}
486 | # var -> 2022-09-07 17:48:46
487 |
488 | # go from a datetime to timestamp
489 | set var (tstamp) = `2022-09-07 17:48:46`
490 | # var -> 1662569326
491 |
492 | # extract the time part of a timestamp
493 | set var (time) = 1662569326
494 | # var -> 17:48:46
495 | ```
496 |
497 | > Note that all variables have a **string** representation and [backticks](https://en.wikipedia.org/wiki/Backtick) are used optionally as delimiters that will be ignored by the interpreter. So you can put backticks around an integer, and you can insert a list in a string.
498 |
499 | ### Here document
500 | **Backstage** supports [here document](https://en.wikipedia.org/wiki/Here_document) for strings inside the `backstage.tasks` file:
501 | ```
502 | [task]
503 | # this is a text with two lines
504 | set text (str) = `First line\nSecond line`
505 |
506 | # this is another text with three lines
507 | set text = `January\nFebruary\nMarch`
508 |
509 | # this is not a text with two lines
510 | set var (raw) = `Hello\nWorld`
511 |
512 | # this is not a text with two lines
513 | set var (str) = `Hello\\nWorld`
514 | ```
515 |
516 | ### Variable interpolation
517 | [Variable interpolation](https://en.wikipedia.org/wiki/String_interpolation) is supported with the ability to access from a list or a string, the value at an arbitrary index, or from a dictionary, the value of a key.
518 |
519 | ```
520 | [task]
521 | # let's play with 'str' variables
522 | set x = `red`
523 | set var = `{x} green blue`
524 | # var -> red green blue
525 |
526 | # get the value of element at index 0
527 | set value = {var[0]}
528 | # value -> r
529 |
530 | ---
531 |
532 | # let's play with a list
533 | set x = `red`
534 | set var (list) = `{x} green blue`
535 |
536 | # get the value of element at index 0
537 | set value = {var[0]}
538 | # value -> red
539 |
540 | # get the value of elements from index 1 to the end
541 | set value = {var[1:]}
542 | # value -> green blue
543 |
544 | ---
545 |
546 | # let's play with a dict
547 | set x (int) = 40 + 2
548 | set var (dict) = `name="John Doe" age={x}`
549 |
550 | # get the value of key 'name'
551 | set value = {var.name}
552 | # value -> John Doe
553 |
554 | # get the value of key 'age'
555 | set value = {var.age}
556 | # value -> 42
557 |
558 | ---
559 |
560 | # cancel the variable interpolation
561 | set var1 = `Hello`
562 | set var2 = `{{var1}} World`
563 | # var2 -> {var1} World
564 | ```
565 |
566 | ## Control flow
567 | **Backstage** implements conditionals and loops. A wide range of operators are available to compare values.
568 | ### Conditionals
569 | ```
570 | [task]
571 | set var1 = 1
572 | set var2 = 1
573 | set x = 2
574 | set regex (raw) = `[\S\s]*?`
575 | set text = `Hello world`
576 | set y = `Hello`
577 |
578 | # conditionals support the classic
579 | # operators: == != > < >= <=
580 | if var1 == 1
581 | $ program1
582 | elif var2 == x
583 | $ program2
584 | else
585 | $ program3
586 |
587 | # Backstage supports logical and/or
588 | if var1 == var2 and var1 >= 3
589 | $ program1
590 |
591 | # use the 'matches' operator
592 | # or the negated one: !matches
593 | if regex matches text
594 | : Matched !
595 | elif regex !matches text
596 | : Mismatched !
597 |
598 | # you can use the in operator
599 | # and also the negated one: !in
600 | if y in text
601 | pass
602 | elif y !in text
603 | pass
604 |
605 | ```
606 |
607 |
608 | ### Loops
609 | ```
610 | [task]
611 |
612 | # From To loop
613 | from 10 to 0
614 | : {R}
615 |
616 | # For loop - iterate over a string
617 | set text = `Hello World`
618 | for char in text
619 | # N is an environment variable that serves as counter
620 | # for all loops
621 | : {N}- {char}
622 |
623 | # For loop - iterate over a list
624 | set data (list) = `red green blue`
625 | for item in data
626 | : {item}
627 |
628 | # For loop - iterate over a dict
629 | set data (dict) = `name="John Doe" age=42`
630 | for item in data
631 | : key -> {item[0]} value -> {item[1]}
632 |
633 | # For loop - iterate over a file
634 | set path = `/home/alex/iliad.txt`
635 | for line in path (file)
636 | : Line {N}
637 | : {line}
638 | :
639 |
640 | # while loop
641 | set var = 1
642 | while var == 1
643 | : One Time Hello
644 | break
645 |
646 | # browse loop
647 | set path = `/home/alex`
648 | browse files and dirs in path
649 | : Directory -> {R}
650 | for item in files
651 | : {item}
652 | for item in dirs
653 | : {item}
654 | ```
655 |
656 |
657 |
658 | # Namespaces and persistence
659 | [Namespaces](https://en.wikipedia.org/wiki/Namespace) are implemented in **Backstage** to provide convenient management of variables by defining three [scopes](https://en.wikipedia.org/wiki/Scope_(computer_science)):
660 | - `L` for **Local** scope;
661 | - `G` for **Global** scope;
662 | - `D` for **Database** scope.
663 |
664 | By default, variables exist in the **Local** namespace and are only accessible to the running task.
665 |
666 | To share data with a subtask, one can expose arbitrary variables that will be copied into the **Global** namespace which is readable and writable (thread-safe) by all subtasks.
667 |
668 | ```
669 | [task]
670 | # by default, variables are defined in Local,
671 | # i.e. they are only visible in the scope of the current task
672 | set var = 42
673 | : Var contains {var}
674 | : Var still contains {L:var}
675 |
676 | # you can make local variables public
677 | expose var
678 | # from now, you can get a thread-safe access to var
679 | # from any running task:
680 | : Global var contains {G:var}
681 |
682 | # branch _task2
683 | & _task2
684 |
685 | [_task2]
686 | : I got {G:var} !
687 |
688 | ```
689 |
690 | Data can also be **persisted**:
691 |
692 | ```
693 | [task]
694 | set var = 42
695 | store var
696 |
697 | # from now, 'var' can be accessed by all tasks
698 | # in this runtime but also in future runtimes
699 | : Var contains {D:var}
700 |
701 | # if you aren't sure about the existence
702 | # of a variable, just do this:
703 | default var
704 |
705 | # it also works with a bunch of variables:
706 | default var1 L:var2 D:var3 G:var4
707 |
708 | # from now, if 'var1' hasn't been manually defined
709 | # by the programmer, it will be
710 | # automatically initialized and
711 | # its value will be an empty string
712 | ```
713 |
714 | Persisted variables are stored in `.backstage/database.json`.
715 |
716 |
717 | # Variable interpolation and escaping
718 | During the string interpolation of a command that spawn processes or branch a subtask, variables that are of the `str` type are automatically [shell-escaped](https://en.wikipedia.org/wiki/Escape_character).
719 |
720 | ```
721 | [task]
722 | # This is a 'str' variable (backticks aren't quotes, by the way!)
723 | set name = `John Doe`
724 | set colors (list) = `red green blue`
725 |
726 | : Welcome {name} !
727 | # Welcome John Doe
728 |
729 | $ program name={name} -c {colors}
730 | # program name='John Doe' -c red green blue
731 |
732 | # Notice the quotes automatically added around the name John Doe
733 | ```
734 |
735 | # Environment variables and language syntax
736 | Environment variables are local to each instance of task. They are defined as uppercase strings. One can edit their contents but can't create new environment variables.
737 |
738 | This is the exhaustive list of environment variables:
739 |
740 | |Variables|Description|
741 | |---|---|
742 | |`ARGS`|List of arguments passed to this task from the command line|
743 | |`CWD`|Current working directory|
744 | |`DATE`|The current date in the **YYYY-MM-DD** format|
745 | |`EMPTY`|Just an empty string|
746 | |`ERROR`|Error string from a process previously spawned|
747 | |`EXCEPTION`|Name of the last exception raised|
748 | |`FALSE`|The integer **0**|
749 | |`HOME`|The path to `$HOME`. Example: `/home/alex`|
750 | |`LINE`|The current line (1-based numbering) of execution in the task body|
751 | |`N`|Counter for `while`, `for`, `from`, and `browse` loops|
752 | |`NOW`|Current timestamp in seconds|
753 | |`ONE`|The integer **1**|
754 | |`OS`|The running operating system: `aix`, `linux`, `win32`, `cygwin`, `darwin`|
755 | |`OUTPUT`|Output string from a process previously spawned|
756 | |`R`|The **return** of Python functions, built-in commands, statements, constructs, or process exit status codes|
757 | |`RANDOM`|Random integer between **0** and **255** (closed interval)|
758 | |`SPACE`|One space ` ` character|
759 | |`STDERR`|Use this variable to perform **STDERR** redirection|
760 | |`STDIN`|Use this variable to perform **STDIN** redirection|
761 | |`STDOUT`|Use this variable to perform **STDOUT** redirection|
762 | |`TASK`|The name of the currently running task|
763 | |`TIME`|The current time in the **HH:MM:SS** format|
764 | |`TIMEOUT`|Timeout in seconds for commands that spawn processes. Default value: **30** seconds|
765 | |`TMP`|Temporary directory. **Attention**, this directory will automatically disappear at the end of the runtime ! So think twice before moving files inside|
766 | |`TRACEBACK`|[Traceback](https://en.wikipedia.org/wiki/Stack_trace#Python) of the last exception raised|
767 | |`TRASH`|Path to the trash: `$HOME/PyrusticData/trash`|
768 | |`TRUE`|The integer **1**|
769 | |`ZERO`|The integer **0**|
770 |
771 | > Note that the `TRACEBACK` and `EXCEPTION` variables are cleared after the next successful command. **Backstage** also generates for convenience, `ARG0`, `ARG1`, `ARGx`, according to the contents of `ARGS`. For example, if `ARGS`contains two arguments, you can expect that `ARG0` and `ARG1` exist.
772 |
773 | ## Language syntax
774 | In this section we will explore the built-in commands, statements, keywords, symbols, and language constructs that make **Backstage**.
775 |
776 | > Note that wherever a built-in command or statement expects a **variable** that will be **read**, for convenience you can instead of supplying a variable name, define an **inline** `int` or `float` literal.
777 |
778 | > Also, consider that the `R` environment variable is your friend, since it is used to cache the data returned by a statement, a construct, or a command.
779 |
780 | ### APPEND
781 | Append data to a file.
782 |
783 | **Usage:** `append to `
784 |
785 |
786 | ### ASSERT
787 | Test is a condition is true.
788 |
789 | **Usage:** `assert (|) (==|!=|<=|>=|<|>|in|!in|rin|!rin|matches|!matches) [and|or] ...`
790 |
791 | **Example:** `assert regex_var matches text_var and var1 in list`
792 |
793 | Note that `!` is used to express negation and `rin` is a Regex-based `in`. A regexly-in operator ;)
794 |
795 |
796 | ### BRANCH
797 | Branch a subtask. The syntax is similar to the one to spawn processes, i.e., a string of words. The syntax supports variable interpolation.
798 |
799 | **Usage:** `& [ ...]`
800 |
801 | **Example:** `& subtask1 name="John Doe" age=42 city={city}`
802 |
803 | > In this example, the `city` variable will be automatically shell-escaped during its interpolation.
804 |
805 |
806 | ### BREAK
807 | Break a loop.
808 |
809 | **Usage:** `break`
810 |
811 | ### BROWSE
812 | Loop construct to browse a directory.
813 |
814 | **Usage:** `browse [files] [and] [dirs] in `
815 |
816 | **Example:**
817 | ```
818 | [task]
819 | browse files and dirs in dirname
820 | : Root -> {R}
821 | for item in files
822 | : {item}
823 | for item in dirs
824 | : {item}
825 |
826 | browse files in dirname
827 | pass
828 |
829 | browse dirs in dirname
830 | pass
831 | ```
832 |
833 | ### CALL
834 | Call a **Python** function from **Backstage** with arguments, then get the return !
835 |
836 | **Usage:** `call .[(, ...)]`
837 |
838 | **Example:**
839 | ```
840 | [task]
841 | # interface with the Python module
842 | interface with package.coffee_module alias coffeemaker
843 |
844 | # call the 'make' function with arguments then get the return
845 | call coffeemaker.make(sugar_cube, extra, 42)
846 | : Result -> {R}
847 | ```
848 |
849 | ### CD
850 | Change directory.
851 |
852 | **Usage:** `cd `
853 |
854 |
855 | ### CHECK
856 | Return the data type (`str`, `list`, `dict`, `int`, `float`) of a variable if it exists, else return an empty string.
857 |
858 | **Usage:** `check `
859 |
860 | **Example:**
861 | ```
862 | [task]
863 | check myvar
864 | if R == EMPTY
865 | : This variable doesn't exist at all !
866 | else
867 | : 'myvar' exists, its data type is {R}
868 | ```
869 |
870 | ### CLEAR
871 | Clear the content of a variable or a list of variables.
872 |
873 | **Usage:** `clear ...`
874 |
875 | **Example:** `clear var1 var2 var3`
876 |
877 | ### COMMENT
878 | Comment.
879 |
880 | **Usage:** `# `
881 |
882 |
883 | ### CONFIG
884 | Read and write configuration options (`FailFast`, `ReportException`, `ShowTraceback`, `TestMode`, `AutoLineBreak`).
885 |
886 | **Usage:** `config ...`
887 |
888 | **Example:**
889 | ```
890 | [task]
891 | config FailFast=1 AutoLineBreak=0
892 | config TestMode
893 | if R == 1
894 | : Test Mode On
895 | elif R == 0
896 | : Test Mode Off
897 | ```
898 |
899 |
900 | ### COPY
901 | Copy a file or a directory tree to a new destination.
902 |
903 | **Usage:** `copy to `
904 |
905 | ### COUNT
906 | Count `chars`, `items`, and `lines` in the content of a variable or inside a file (if the `(file)` tag is applied).
907 |
908 | **Usage:** `count (chars|items|lines) in (|) [(file)]`
909 |
910 | **Example:**
911 | ```
912 | [task]
913 | set path = /home/alex/iliad.txt
914 | count chars in path (file)
915 | if R == 0
916 | : The file is empty !
917 | ```
918 |
919 | ### CREATE
920 | Create a new file or directory.
921 |
922 | **Usage:** `create (dir|file) `
923 |
924 | ### DEFAULT
925 | Define an empty variable (or a bunch of variables) if it doesn't exist yet in the namespace.
926 |
927 | **Usage:** `default ...`
928 |
929 | **Example:**
930 |
931 | ```
932 | [task]
933 | # default two variables in the Local namespace
934 | default var1 L:var2
935 |
936 | # default one variable in the Database namespace
937 | default D:name
938 |
939 | # from now, 'D:name' can be safely accessed
940 | # even though the 'database.json' file supposed
941 | # to contain the 'name' value was inadvertently deleted.
942 | ```
943 |
944 | ### DROP
945 | Destroy a variable (or a bunch of variables).
946 |
947 | **Usage:** `drop ...`
948 |
949 | ### ELIF
950 | Part of the `if` conditional construct.
951 |
952 | **Usage:** `elif (|) (==|!=|<=|>=|<|>|in|!in|rin|!rin|matches|!matches) [and|or] ...`
953 |
954 | ### ELSE
955 | Part of the `if` conditional construct.
956 |
957 | **Usage:** `else`
958 |
959 | ### ENTER
960 | Invite user to submit data.
961 |
962 | **Usage:** `> [ [: ]]`
963 |
964 | **Example:**
965 | ```
966 | [task]
967 |
968 | > name : Please enter your name
969 | # have you spotted the space at the end the line above ?
970 |
971 | # the same line can be rewritten like this:
972 | > name : `Please enter your name `
973 | # backticks serve as delimiters that will be ignored
974 |
975 | # this one is also possible:
976 | set msg = `Please enter your name `
977 | > name : {msg}
978 |
979 | # even this:
980 | set info (dict) = name="John Doe" age=42
981 | > info.name : `Please enter your name`
982 | ```
983 |
984 | ### EXIT
985 | Exit.
986 |
987 | **Usage:** `exit`
988 |
989 | ### EXPOSE
990 | Copy a variable (or a bunch of variables) into the **Global** namespace.
991 |
992 | **Usage:** `expose ...`
993 |
994 | ### FAIL
995 | Deliberately fail. It breaks the running task and mark it as a failure.
996 |
997 | **Usage:** `fail`
998 |
999 | ### FIND
1000 | Find files and or directories paths.
1001 |
1002 | **Usage 1:** `find [all] (paths|files|dirs) in `
1003 |
1004 | **Usage 2:** `find ... matching `
1005 |
1006 | **Usage 3:** `find ... [and] (accessed|modified|created) (at|after|before|between) [and ]`
1007 |
1008 | **Example:** `find files in dirname matching regex and accessed between timestamp1 and timestamp2`
1009 |
1010 | ### FOR
1011 | A `for` loop to iterate the content of a variable or the content of a file (if you apply the `(file)` tag).
1012 |
1013 | **Usage:** `for (char|item|line) in (|) [(file)]`
1014 |
1015 | **Example:**
1016 | ```
1017 | [task]
1018 | # this code iterates over each character of the Iliad,
1019 | # and outputs it as it,
1020 | # with one twist: each line starts with its index (0-based)
1021 |
1022 | set path = `/home/alex/iliad.txt`
1023 |
1024 | # the print statement (:) won't anymore
1025 | # automatically add a line break !
1026 | config AutoLineBreak=0
1027 |
1028 | for line in path (file)
1029 | : `{N} `
1030 | for char in line
1031 | : {char}
1032 | : \n
1033 | ```
1034 |
1035 |
1036 | ### FROM
1037 | A loop to go from an integer to another one. If the `start` integer is superior to the `end` integer, the count will decrease.
1038 |
1039 | **Usage:** `from to `
1040 |
1041 | **Example:**
1042 | ```
1043 | [task]
1044 | from 10 to 0
1045 | # here, N will go from 0 to 10
1046 | # but R will go from 10 to 0
1047 | # because N is a counter for all loops
1048 | # while R is a cache for whatever is returned
1049 | # by a command, a statement, or a construct
1050 | : {N}\t{R}
1051 |
1052 | # As you can see, I can add a Tab \t since
1053 | # Backstage supports natively here document ;)
1054 |
1055 | # To get a simple backslash followed by a 't': \\t
1056 | ```
1057 |
1058 | ### GET
1059 | Get a `char`, an `item`, or a `line` at index `x` (including negative index) from a target. The target can be the content of a variable or a file (if you apply the `(file)` tag).
1060 |
1061 | **Usage:** `get (char|item|line) from (|) [(file)]`
1062 |
1063 | ### IF
1064 | Conditional construct.
1065 |
1066 | **Usage:** `if (|) (==|!=|<=|>=|<|>|in|!in|rin|!rin|matches|!matches) [and|or] ...`
1067 |
1068 | **Example:**
1069 | ```
1070 | [task]
1071 | default var1 var2 var3 var4
1072 | if var1 == var2 or var3 == var4
1073 | pass
1074 | elif EMPTY == EMPTY
1075 | pass
1076 | else
1077 | pass
1078 | ```
1079 |
1080 | ### INTERFACE
1081 | Interface with a **Python** module.
1082 |
1083 | **Usage:** `interface with [.] [alias ]`
1084 |
1085 | **Example:**
1086 |
1087 | ```
1088 | [task]
1089 | interface with package.mymodule alias module
1090 | default var1 var2
1091 | call module.function(var1, var2)
1092 | ```
1093 |
1094 | ### LINE
1095 | Draw a line.
1096 |
1097 | **Usage:** `(=|-) ...`
1098 |
1099 | **Example:** `----------` or `==========`
1100 |
1101 |
1102 | ### MOVE
1103 | Move a file or a directory tree to a new destination.
1104 |
1105 | **Usage:** `move to `
1106 |
1107 | ### PASS
1108 | Placeholder for the code that you might write in the future. This statement does nothing. It is the same as the eponymous one in **Python**.
1109 |
1110 | **Usage:** `pass`
1111 |
1112 | ### POKE
1113 | Poke a file or directory to get access to a `dict` of properties if this path exists. Available properties: `size` `mtime` `ctime` `atime` `nlink` `uid` `gid` `mode` `ino` `dev`.
1114 |
1115 | **Usage:** `poke `
1116 |
1117 | **Example:**
1118 | ```
1119 | [task]
1120 | set path = /home/alex/iliad.txt
1121 | poke path
1122 | if R == EMPTY
1123 | : Oops ! This file doesn't exist
1124 | else
1125 | : File size -> {R.size}
1126 | ```
1127 |
1128 | ### PREPEND
1129 | Prepend data to a file
1130 |
1131 | **Usage:** `prepend to `
1132 |
1133 | ### PRINT
1134 | Print data. You can use backquotes as delimiters. This statement supports variable interpolation.
1135 |
1136 | **Usage:** `: `
1137 |
1138 | **Example:**
1139 | ```
1140 | [task]
1141 | : Hello World !
1142 | # Have you spotted the two extra spaces characters ?
1143 |
1144 | : ` Hello World ! `
1145 | # hehehe, got you ! ;)
1146 | ```
1147 |
1148 | ### PUSH
1149 | Push variables into the input of the next spawned process.
1150 |
1151 | **Usage:** `push ...`
1152 |
1153 | ### READ
1154 | Read all or a specific line index (including negative index) from a file.
1155 |
1156 | **Usage:** `read (*|) from `
1157 |
1158 | ### REPLACE
1159 | Replace some pattern in a text with a replacement value.
1160 |
1161 | **Usage:** `replace in with `
1162 |
1163 | ### RETURN
1164 | Return from a task with a value.
1165 |
1166 | **Usage:** `return []`
1167 |
1168 | ### SET
1169 | Define a new variable or update the content of an existing variable. You don't can't specify a data type, but instead you can apply an assigment tag that is one of: `(raw)` `(str)` `(list)` `(dict)` `(int)` `(float)` `(date)` `(time)` `(dtime)` `(tstamp)`. Note that backquotes can be used as delimiters for the value (right side of the equal sign). These delimiters will be ignored. Backticks aren't quotes. This statement supports variable interpolation.
1170 |
1171 | **Usage:** `set [(raw)|(str)|(list)|(dict)|(int)|(float)|(date)|(time)|(dtime)|(tstamp))] = `
1172 |
1173 | **Example:** `set var (int) = 1 + 2`
1174 |
1175 |
1176 | ### SLEEP
1177 | Sleep for `x` seconds.
1178 |
1179 | **Usage:** `sleep `
1180 |
1181 | ### SPAWN
1182 | Spawn a new process.
1183 |
1184 | **Usage:** `$ [ ...]`
1185 |
1186 | **Example:** `$ program1 arg {var} | program2 `
1187 |
1188 | ### SPLIT
1189 | Split with a regex pattern a text into a list.
1190 |
1191 | **Usage:** `split with `
1192 |
1193 | ### SPOT
1194 | Count the number of occurrences of a regex pattern inside a text.
1195 |
1196 | **Usage:** `spot in `
1197 |
1198 | ### STORE
1199 | Store a variable (or a bunch of variables) in the **Database** namespace. A stored variable can be accessed like this: `D:var`
1200 |
1201 | **Usage:** `store ...`
1202 |
1203 | ### THREAD
1204 | Branch a subtask... but in a new thread.
1205 |
1206 | **Usage:** `~ [ ...]`
1207 |
1208 | ### WHILE
1209 | The `while` loop. Use `break` to break it, and check `N` if you need a counter.
1210 |
1211 | **Usage:** `while (|) (==|!=|<=|>=|<|>|in|!in|rin|!rin|matches|!matches) [and|or] ...`
1212 |
1213 | ### WRITE
1214 | Erase the content of a file to write some data inside.
1215 |
1216 | **Usage:** `write to `
1217 |
1218 |
1219 | # File and directory manipulation
1220 | Let's explore how file and directory manipulatin is performed with **Backstage**.
1221 |
1222 | ## Resource creation
1223 |
1224 | Create a file:
1225 | ```
1226 | [task]
1227 | # create a file
1228 | set path = /home/alex/iliad.txt
1229 | create file path
1230 | ```
1231 |
1232 | Create a directory:
1233 |
1234 | ```
1235 | [task]
1236 | # create a directory
1237 | set path = /home/alex/new/directory
1238 | create dir path
1239 | ```
1240 |
1241 | ## File edition
1242 |
1243 | ```
1244 | [task]
1245 | set path = /home/alex/iliad.txt
1246 | set var = Hello World
1247 |
1248 | # write data
1249 | write var to path
1250 |
1251 | # append data to a file
1252 | append var to path
1253 |
1254 | # prepend data to a file
1255 | prepend var to path
1256 | ```
1257 |
1258 | ## Read the content of a file
1259 | ```
1260 | [task]
1261 | set path = /home/alex/iliad.txt
1262 |
1263 | # read all from 'iliad.txt'
1264 | read * from path
1265 | : {R}
1266 |
1267 | # read the line at index 3
1268 | set index (int) = 1 + 1 + 1
1269 | read index from path
1270 | : {R}
1271 |
1272 | # just want to read the last line ?
1273 | read -1 from path
1274 | : {R}
1275 | ```
1276 |
1277 | ## Iterating the content of a file
1278 | ```
1279 | [task]
1280 | set path = /home/alex/iliad.txt
1281 |
1282 | # iterate over the characters in a file
1283 | for char in path (file)
1284 | : Character -> char
1285 |
1286 | # iterate over the lines in a file
1287 | for line in path (file)
1288 | : {line}
1289 | ```
1290 |
1291 | ## Browse a folder
1292 |
1293 | ```
1294 | [task]
1295 | set folder = /home/alex
1296 |
1297 | browse files and dirs in folder
1298 | : Directory -> {R}
1299 | for item in files
1300 | : {item}
1301 | for item in dirs
1302 | : {item}
1303 | ```
1304 |
1305 | ## Find resources
1306 | The `find` statement is like Glob but on steroid:
1307 | ```
1308 | [task]
1309 | set folder = /home/alex
1310 | set regex (raw) = `[\S\s]*?`
1311 | set timestamp1 = 1223322233
1312 |
1313 | find files in folder matching regex and accessed between timestamp1 and NOW
1314 | : Results -> {R}
1315 |
1316 | ```
1317 |
1318 | ## Read resource properties
1319 | You can get from **Backstage** the properties of an arbitrary resource, like its size:
1320 |
1321 | ```
1322 | [task]
1323 | set path = /home/alex/iliad.txt
1324 |
1325 | # poke a file
1326 | poke path
1327 | : Creation timestamp -> {R.ctime}
1328 | : Size -> {R.size}
1329 |
1330 | ```
1331 |
1332 |
1333 | # Interfacing with Python
1334 | Interfacing with **Python** is as simple as this:
1335 | ```
1336 | [task]
1337 | interface with python.module as my_module
1338 | set name = `John Doe`
1339 | set age (int) = 40 + 2
1340 | call my_module.function(name, age)
1341 | : Return -> {R}
1342 | ```
1343 |
1344 | > **Allowed return data types:** `str`, `list` (one-dimensional),`tuple` (one-dimensional), `dict` (one-dimensional), `int`, and `float`. **Python** functions can also return `True`, `False`, and `None`, which will be converted to **1**, **0** and an **empty string**, respectively.
1345 |
1346 | # Exception handling and tests
1347 | Whenever an exception is raised, the variables `EXCEPTION` and `TRACEBACK` are updated and **Backstage** continues calmly its execution.
1348 |
1349 | Note that the variables `TRACEBACK` and `EXCEPTION` are cleared after the next successful command.
1350 |
1351 | If you want the execution to stop whenever an exception is raised, just set `1` to the `FailFast` configuration option.
1352 |
1353 | ```
1354 | [task]
1355 | config FailFast=1
1356 | ```
1357 |
1358 | If you want to read a report of an exception when it's raised, just set `1` to the `ReportException` configuration option.
1359 |
1360 | ```
1361 | [task]
1362 | config ReportException=1
1363 | ```
1364 |
1365 | If you want to read the verbose [traceback](https://en.wikipedia.org/wiki/Stack_trace#Python) of an exception when it's raised, just set `1` to the `ShowTraceback` configuration option.
1366 |
1367 | ```
1368 | [task]
1369 | config ShowTraceback=1
1370 | ```
1371 |
1372 | You can edit these configuration options in the same command:
1373 |
1374 | ```
1375 | [task]
1376 | config FailFast=1 ReportException=1 ShowTraceback=0
1377 | ```
1378 | You can read the current value of an arbitrary configuration option:
1379 |
1380 | ```
1381 | [task]
1382 | config FailFast
1383 | : FailFast -> {R}
1384 | ```
1385 |
1386 | ## Debug mode
1387 | Instead of manually setting the `ReportException` configuration option to `1`, you can simply run a task in debug mode:
1388 |
1389 | ```bash
1390 | $ backstage -d task arg
1391 | ```
1392 |
1393 | ## Tests
1394 | To create a test, just postfix `.test` to the name of a task. Then from the command line, just run the test `backstage --test task`.
1395 |
1396 | ### Example
1397 | ```
1398 | [task]
1399 | set val (int) = {ARGS[0]} + {ARGS[1]}
1400 | return val
1401 |
1402 | [task.test]
1403 | # here we branch the task with the arguments 40 and 2
1404 | & task 40 2
1405 | # we expect 42 as return
1406 | assert R == 42
1407 | ```
1408 |
1409 | # Command line interface and developer experience
1410 |
1411 | ```bash
1412 | $ backstage --help
1413 | Welcome to Pyrustic Backstage !
1414 | Ultimate task automation tool for hackers.
1415 |
1416 | Usage:
1417 | backstage
1418 | backstage [ ...]
1419 | backstage [ ...]
1420 |
1421 | Options:
1422 | -i, --intro Show file introductory text
1423 | -c, --check Show the list of tasks
1424 | -C, --Check Show the descriptive list of tasks
1425 | -d, --debug [ ...] Run task in debug mode
1426 | -t, --test [ ...] Run tests
1427 | -T, --Test [ ...] Run tests in debug mode
1428 | -s, --search Search for a task by its name
1429 | -S, --Search Search for a task by keyword
1430 | -h, --help [] Show help text
1431 |
1432 | The string can use a glob-like syntax that allows
1433 | wildcards '*' and '?'. Therefore, 'task1' is identical to 'task*'.
1434 |
1435 | Visit the webpage: https://github.com/pyrustic/backstage
1436 | ```
1437 |
1438 | ## Developer experience
1439 | **Backstage** will do its best to help you understand raised exceptions:
1440 |
1441 | ```bash
1442 | $ backstage task1
1443 | ZeroDivisionError at line 3 of [task1] !
1444 | division by zero
1445 |
1446 | $ backstage task2
1447 | InterpretationError at line 7 of [task2] !
1448 | Usage: sleep
1449 | ```
1450 |
1451 | When you run **Backstage** in the loop mode, you can enjoy the autocomplete functionality (use the Tab key to complete your input) and also the history functionality (use Up and Down arrows).
1452 |
1453 | ```bash
1454 | $ backstage
1455 | Welcome to Pyrustic Backstage !
1456 | Ultimate task automation tool for hackers.
1457 | Press 'Ctrl-c' or 'Ctrl-d' to quit.
1458 | Type '--help' or '-h' to show more information.
1459 |
1460 | (backstage) task(Tab Tab)
1461 | task task1 task2 task3
1462 |
1463 | (backstage) --h(Tab)
1464 |
1465 | ...
1466 |
1467 | ```
1468 |
1469 | # Miscellaneous
1470 | In the following subsections, we will explore some miscellaneous information.
1471 |
1472 | ## Dogfooding
1473 | **Backstage** itself as a project relies on a `backstage.tasks` file (check the root of this repository). You are reading a document about **Backstage** that has been updated with **Backstage** !
1474 |
1475 | ## Dependencies
1476 | **Backstage** relies on these **Python** packages:
1477 | - [Subrun](https://github.com/pyrustic/subrun) to spawn new processes;
1478 | - [Shared](https://github.com/pyrustic/shared) to store data;
1479 | - [Jesth](https://github.com/pyrustic/jesth) to parse `backstage.tasks` files;
1480 | - [Oscan](https://github.com/pyrustic/jesth) to extract tokens from the script.
1481 |
1482 | ## Indentation
1483 | Four (4) spaces by [indent](https://en.wikipedia.org/wiki/Indentation_(typesetting)#Indentation_in_programming). Period.
1484 |
1485 | ## Python 3
1486 | Inside the script file, you don't have to type `python3` in a command to spawn the **Python** interpreter. Just type `python` to spawn the same interpreter that is running **Backstage**:
1487 | ```
1488 | [task]
1489 | $ python -m my.package.module
1490 | ```
1491 |
1492 | ## Shell
1493 | **Backstage** doesn't rely on any **Shell**. But you can still pipe commands !
1494 |
1495 | Example, if you want to change the current working directory:
1496 | ```
1497 | [task]
1498 | # instead of doing this
1499 | $ cd {HOME}
1500 |
1501 | # use the built-in 'cd'
1502 | cd HOME
1503 |
1504 | # you can still spawn programs commonly used in the shell
1505 | $ ls
1506 |
1507 | # or make this complex stuff (successfully performed on Ubuntu)
1508 | $ python -m this | tail --lines=+3 | sort
1509 | ```
1510 |
1511 | ## Data cache
1512 | **Backstage** stores data in an automatically created directory `.backstage` located in the current working directory. Inside this directory you can find the `execution.log` and `database.json` files.
1513 |
1514 | ## Automatic line break
1515 | If you don't want anymore an extra line break at the end of printed strings, you can turn off this functionality:
1516 |
1517 | ```
1518 | [task]
1519 | # turn off auto line break
1520 | config AutoLineBreak=0
1521 | : `Hello `
1522 | : `World`
1523 | # turn on auto line break
1524 | config AutoLineBreak=1
1525 | : Hello World
1526 | ```
1527 |
1528 |
1529 | ## Lines
1530 | You can draw lines with the characters `=` or `-`. If you pick one, only this one is allowed to appear on the same line.
1531 | ```
1532 | [task1]
1533 |
1534 | $ program1
1535 | $ program2
1536 |
1537 | -----------------
1538 |
1539 | [task2]
1540 | pass
1541 |
1542 | =================
1543 | ```
1544 |
1545 |
1546 | # Demo
1547 | The demo is a [repository](https://github.com/pyrustic/project) that contains a [backstage.tasks](https://github.com/pyrustic/project/blob/master/backstage.tasks#L1) file similar to the one used to build, package and publish my projects. Your mission, if you accept it, is to clone the demo repository and run the `backstage.tasks` which contains tasks to create a new **Python** `Hello Friend !` project, build it, perform versioning, init **Git** , perform **Git** Commit and **Git** Push, and even push the latest built package to [PyPI](https://pypi.org) !
1548 |
1549 | ```bash
1550 | # 1- clone the repository
1551 | $ git clone https://github.com/pyrustic/project
1552 | $ cd project
1553 |
1554 | # 2- install backstage
1555 | $ pip install backstage
1556 |
1557 | # 3- install buildver
1558 | $ pip install buildver
1559 |
1560 | # 4- install setupinit
1561 | $ pip install setupinit
1562 |
1563 | # 5- list the tasks available in the `backstage.tasks` file
1564 | $ backstage -c
1565 | Available tasks (11):
1566 | build check clean gendoc gitcommit gitinit gitpush init
1567 | release test upload2pypi
1568 |
1569 | # 6- descriptive list of tasks
1570 | $ backstage -C
1571 |
1572 | # 7- initialize the project
1573 | $ backstage init
1574 | Successfully initialized !
1575 |
1576 | # 8- run the project
1577 | $ python3 -m project
1578 | Hello Friend !
1579 |
1580 | # 9- build the project
1581 | $ backstage build
1582 | building v0.0.1 ...
1583 | Successfully built 'project' v0.0.1 !
1584 | VERSION file updated from 0.0.1 to 0.0.2
1585 |
1586 | # 10- check the project
1587 | $ backstage check
1588 | project v0.0.2 (source)
1589 | .whl v0.0.1 (package) built 28 secs ago
1590 |
1591 | # 11- initialize Git
1592 | $ backstage gitinit
1593 | Origin: https://github.com/pyrustic/project.git
1594 |
1595 | # 12- perform a Git Commit
1596 | $ backstage gitcommit
1597 |
1598 | # 13- perform a Git Push
1599 | $ backstage gitpush
1600 |
1601 | # 14- upload to PyPI
1602 | $ backstage upload2pypi
1603 | ```
1604 |
1605 | > **Note:** Commands `9`, `12`, `13`, and `14` can be replaced with one command: `backstage release`
1606 |
1607 |
1608 |
1609 | # Installation
1610 | **Backstage** is **cross platform** and versions under **1.0.0** will be considered **Beta** at best. It is built on [Ubuntu](https://ubuntu.com/download/desktop) with [Python 3.8](https://www.python.org/downloads/) and should work on **Python 3.5** or **newer**.
1611 |
1612 | ## For the first time
1613 |
1614 | ```bash
1615 | $ pip install backstage
1616 | ```
1617 |
1618 | ## Upgrade
1619 | ```bash
1620 | $ pip install backstage --upgrade --upgrade-strategy eager
1621 |
1622 | ```
1623 |
1624 |
1625 |
1626 |
1627 |
1628 | [Back to top](#readme)
1629 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.23
--------------------------------------------------------------------------------
/backstage.tasks:
--------------------------------------------------------------------------------
1 | 2022-09-15 (Alex Rustic)
2 | This 'backstage.tasks' file is written for Python projects.
3 | Put this file in the root directory of your Python project.
4 |
5 | You will need to install 'backstage', 'setupinit', and 'buildver':
6 | $ pip install backstage
7 | $ pip install setupinit
8 | $ pip install buildver
9 |
10 | Once installed, run:
11 | $ cd /path/to/project
12 | $ backstage --help
13 |
14 | Visit https://github.com/pyrustic/backstage
15 |
16 |
17 | ------------------------------------------------------------------
18 |
19 |
20 | [build]
21 | $ buildver build {ARGS}
22 |
23 |
24 | [build.test]
25 | assert 1 == 1
26 |
27 |
28 | ------------------------------------------------------------------
29 |
30 |
31 | [check]
32 | $ buildver check
33 |
34 |
35 | [check.test]
36 | assert 1 == 1
37 |
38 |
39 | ------------------------------------------------------------------
40 |
41 |
42 | [clean]
43 | pass
44 |
45 |
46 | [clean.test]
47 | assert 1 == 1
48 |
49 |
50 | ------------------------------------------------------------------
51 |
52 |
53 | [gendoc]
54 | $ hyperdoc build
55 |
56 |
57 | [gendoc.test]
58 | assert 1 == 1
59 |
60 |
61 | ------------------------------------------------------------------
62 |
63 |
64 | [gitcommit]
65 | default message
66 | set message = {ARGS[0]}
67 | if message == EMPTY
68 | set message = `Update`
69 | $ git add .
70 | $ git commit -m {message}
71 |
72 |
73 | [gitcommit.test]
74 | assert 1 == 1
75 |
76 |
77 | ------------------------------------------------------------------
78 |
79 |
80 | [gitinit]
81 | default origin
82 | set origin = {ARGS[0]}
83 | if origin == EMPTY
84 | > origin : `Origin: `
85 | $ git init
86 | $ git remote add origin {origin}
87 |
88 |
89 | [gitinit.test]
90 | assert 1 == 1
91 |
92 |
93 | ------------------------------------------------------------------
94 |
95 |
96 | [gitpush]
97 | default project_version
98 | set project_version = {ARGS[0]}
99 | if project_version == EMPTY
100 | $ git push origin master
101 | else
102 | $ git push origin master tag {project_version}
103 | return R
104 |
105 |
106 | [gitpush.test]
107 | assert 1 == 1
108 |
109 |
110 | ------------------------------------------------------------------
111 |
112 |
113 | [init]
114 | $ setupinit init
115 |
116 |
117 | [init.test]
118 | assert 1 == 1
119 |
120 |
121 | ------------------------------------------------------------------
122 |
123 |
124 | [release]
125 | interface with buildver
126 | set project_dir = {CWD}
127 | call buildver.get_version(project_dir)
128 | set project_version = {R}
129 | if project_version == EMPTY
130 | : Failed to get the project version.
131 | : Release cancelled.
132 | return
133 | : Project version: {project_version}
134 | :
135 | : == Documentation generation ==
136 | :
137 | & gendoc
138 | :
139 | : == Commit changes ==
140 | :
141 | & gitcommit
142 | :
143 | : == Git tag ==
144 | :
145 | $ git tag {project_version}
146 | :
147 | : == Build distribution package ==
148 | :
149 | & build {ARGS}
150 | :
151 | : == Commit changes ==
152 | :
153 | & gitcommit
154 | :
155 | : == Git push ==
156 | :
157 | & _looped_task gitpush {project_version}
158 | :
159 | : == Upload package to PyPI ==
160 | :
161 | & _looped_task upload2pypi
162 | :
163 | : Goodbye !
164 |
165 |
166 | [release.test]
167 | assert 1 == 1
168 |
169 |
170 | ------------------------------------------------------------------
171 |
172 |
173 | [test]
174 | $ python -m unittest discover -f -s tests -t .
175 |
176 |
177 | [test.test]
178 | assert 1 == 1
179 |
180 |
181 | ------------------------------------------------------------------
182 |
183 |
184 | [upload2pypi]
185 | $ twine upload --skip-existing dist/*
186 | return R
187 |
188 |
189 | [upload2pypi.test]
190 | assert 1 == 1
191 |
192 |
193 | ------------------------------------------------------------------
194 |
195 |
196 | [_looped_task]
197 | # take a task as argument
198 | # and ensure it exits with success
199 | # (if the user is ok with it)
200 | set success = {FALSE}
201 | while success == FALSE
202 | & {ARGS}
203 | if R == 0
204 | set success = {TRUE}
205 | else
206 | set success = {FALSE}
207 | if success == FALSE
208 | :
209 | & _ask_confirmation "Repeat the failed subtask (y/N): "
210 | :
211 | if R == 0
212 | break
213 |
214 |
215 | ------------------------------------------------------------------
216 |
217 |
218 | [_ask_confirmation]
219 | # ask for confirmation
220 | # returns 1 if True, else 0
221 | default message
222 | set message = {ARGS[0]}
223 | > result : {message}
224 | set yes = `y`
225 | if result == yes
226 | return 1
227 | return 0
228 |
229 |
230 | ------------------------------------------------------------------
231 |
232 |
233 | [build.help]
234 | Build the Python project
235 |
236 | Usage:
237 | backstage build
238 | backstage build then
239 |
240 | Example:
241 | backstage build then 3.0.0
242 | backstage build then +maj
243 |
244 | Under the hood, the package 'buildver' is used.
245 |
246 |
247 | ------------------------------------------------------------------
248 |
249 |
250 | [check.help]
251 | Get the project version and latest build information
252 |
253 | Usage:
254 | backstage check
255 |
256 | Under the hood, the package 'buildver' is used.
257 |
258 |
259 | ------------------------------------------------------------------
260 |
261 |
262 | [clean.help]
263 | Clean the project directory
264 |
265 | Usage:
266 | backstage clean
267 |
268 |
269 | ------------------------------------------------------------------
270 |
271 |
272 | [gendoc.help]
273 | Generate the project documentation
274 |
275 | Usage:
276 | backstage gendoc
277 |
278 |
279 | ------------------------------------------------------------------
280 |
281 |
282 | [gitcommit.help]
283 | Save your changes to the local repository
284 |
285 | Usage:
286 | backstage gitcommit
287 | backstage gitcommit
288 |
289 | Note: by default, the message is set to "Update".
290 |
291 | Under the hood, the program Git is used.
292 |
293 |
294 | ------------------------------------------------------------------
295 |
296 |
297 | [gitinit.help]
298 | Initialize a new Git repository then create a new connection
299 | to the remote repository
300 |
301 | Usage:
302 | backstage gitinit
303 |
304 | Under the hood, the program Git is used.
305 |
306 |
307 | ------------------------------------------------------------------
308 |
309 |
310 | [gitpush.help]
311 | Send the commits from your local Git repository to the remote repository
312 |
313 | Usage:
314 | backstage gitpush
315 | backstage gitpush
316 |
317 | Under the hood, the program Git is used.
318 |
319 |
320 | ------------------------------------------------------------------
321 |
322 |
323 | [init.help]
324 | Initialize the Python project
325 |
326 | Usage:
327 | backstage init
328 |
329 | Under the hood, the package 'setupinit' is used.
330 |
331 |
332 | ------------------------------------------------------------------
333 |
334 |
335 | [release.help]
336 | Release a new version of this project
337 |
338 | Usage:
339 | backstage release
340 | backstage release then
341 |
342 | Example:
343 | backstage release then 3.0.1
344 | backstage release then +maj
345 |
346 | Under the hood, 'Git', 'buildver' and 'twine' are used.
347 |
348 |
349 | ------------------------------------------------------------------
350 |
351 |
352 | [test.help]
353 | Run tests
354 |
355 | Usage:
356 | backstage test
357 |
358 | Under the hood, the module 'unittest' is used.
359 |
360 |
361 | ------------------------------------------------------------------
362 |
363 |
364 | [upload2pypi.help]
365 | Upload the recently built distribution package to PyPI
366 |
367 | Usage:
368 | backstage upload2pypi
369 |
370 | Under the hood, the package 'twine' is used.
371 |
--------------------------------------------------------------------------------
/backstage/__init__.py:
--------------------------------------------------------------------------------
1 | """Project Backstage API"""
2 | import os
3 | import os.path
4 | import sys
5 | import json
6 | from threading import Lock
7 | from backstage import util
8 | from backstage.pattern import Pattern
9 | from backstage import error
10 | from backstage.runner import Runner
11 | from backstage import constant
12 |
13 |
14 | class Backstage:
15 | def __init__(self, directory):
16 | self._directory = directory
17 | self._cache_dir = os.path.join(directory, ".backstage")
18 | self._tasks = None
19 | self._runners = list()
20 | self._global_vars = dict()
21 | self._database_vars = dict()
22 | self._lock = Lock()
23 | self._i = 0
24 | self._execution_log = list()
25 | self._setup()
26 |
27 | @property
28 | def directory(self):
29 | return self._directory
30 |
31 | @property
32 | def tasks(self):
33 | return self._tasks
34 |
35 | @property
36 | def global_vars(self):
37 | return self._global_vars
38 |
39 | @property
40 | def database_vars(self):
41 | return self._database_vars
42 |
43 | @property
44 | def runners(self):
45 | return self._runners
46 |
47 | @property
48 | def lock(self):
49 | return self._lock
50 |
51 | @property
52 | def execution_log(self):
53 | return self._execution_log
54 |
55 | def run(self, task, arguments=None, config=None):
56 | """
57 | task is a string
58 | arguments is either None, a string or a list of strings
59 | """
60 | runner = Runner(self, task, self.gen_rid(), arguments, config)
61 | try:
62 | runner.start()
63 | except Exception as e:
64 | pass
65 | # store data
66 | filename = os.path.join(self._cache_dir, "database.json")
67 | with open(filename, "w") as file:
68 | json.dump(self._database_vars, file)
69 | # store execution log
70 | util.save_execution_log(self, self._execution_log)
71 | return runner
72 |
73 | def gen_rid(self):
74 | with self._lock:
75 | rid = self._i
76 | self._i += 1
77 | return rid
78 |
79 | def _setup(self):
80 | # allow python modules imports from backstage.tasks
81 | sys.path.insert(0, self._directory)
82 | # create cache_dir
83 | db_filename = os.path.join(self._cache_dir, "database.json")
84 | if not os.path.isdir(self._cache_dir):
85 | try:
86 | os.makedirs(self._cache_dir)
87 | except Exception as e:
88 | pass
89 | with open(db_filename, "w") as file:
90 | json.dump(self._database_vars, file)
91 | # load tasks
92 | tasks = util.get_tasks(self._directory)
93 | if not tasks:
94 | return
95 | self._tasks = {key: val for key, val in tasks.items() if not key.endswith(".doc")}
96 | # load stored vars
97 |
98 | with open(db_filename, "r") as file:
99 | database_vars = json.load(file)
100 | self._database_vars = database_vars if database_vars else dict()
101 |
--------------------------------------------------------------------------------
/backstage/__main__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import os.path
3 | import sys
4 | from backstage.cli import Cli
5 |
6 |
7 | __all__ = []
8 |
9 |
10 | def main():
11 | directory = os.getcwd()
12 | command = sys.argv[1:]
13 | cli = Cli(directory)
14 | if command:
15 | cli.run(command)
16 | return
17 | cli.loop()
18 |
19 |
20 | if __name__ == "__main__":
21 | main()
22 |
--------------------------------------------------------------------------------
/backstage/cli/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import os.path
3 | import shlex
4 | import textwrap
5 | import readline
6 | import atexit
7 | import re
8 | from backstage import Backstage, constant, util, text
9 |
10 |
11 | class Cli:
12 | def __init__(self, directory):
13 | self._directory = directory
14 | self._backstage = Backstage(directory)
15 | self._tasks = dict()
16 | self._help_docs = dict()
17 | self._tests = dict()
18 | self._intro = None
19 | self._setup()
20 |
21 | @property
22 | def directory(self):
23 | return self._directory
24 |
25 | @property
26 | def backstage(self):
27 | return self._backstage
28 |
29 | @property
30 | def tasks(self):
31 | return self._tasks
32 |
33 | def run(self, command):
34 | """command is either a string or a list"""
35 | if not command:
36 | return
37 | if isinstance(command, str):
38 | command = shlex.split(command, posix=True)
39 | name = command[0]
40 | args = command[1:]
41 | if not self._tasks and name not in ("-h", "--help"):
42 | print("There is no 'backstage.tasks' file in this directory !")
43 | return
44 | if name in ("-i", "--intro"):
45 | self._print_intro(*args)
46 | elif name in ("-c", "--check"):
47 | self._print_tasks_list(*args)
48 | elif name in ("-C", "--Check"):
49 | self._print_descriptive_tasks_list(*args)
50 | elif name in ("-d", "--debug"):
51 | if not args:
52 | print("Incomplete command, please submit a task name.")
53 | return
54 | name = args[0]
55 | args = args[1:]
56 | self._run_task(name, *args, debug=True)
57 | elif name in ("-t", "--test"):
58 | self._run_tests(*args)
59 | elif name in ("-T", "--Test"):
60 | self._run_tests(*args, debug=True)
61 | elif name in ("-s", "--search"):
62 | self._search_task(*args)
63 | elif name in ("-S", "--Search"):
64 | self._search_keyword(*args)
65 | elif name in ("-h", "--help"):
66 | self._print_help_text(*args)
67 | else:
68 | self._run_task(name, *args)
69 |
70 | def loop(self):
71 | if not self._tasks:
72 | print("There is no 'backstage.tasks' file in this directory !")
73 | return
74 | print(text.INTRO)
75 | print("Press 'Ctrl-c' or 'Ctrl-d' to quit.")
76 | print("Type '--help' or '-h' to show more information.")
77 | print()
78 | while True:
79 | self._activate_autocomplete()
80 | entry = self._wait_input()
81 | if entry is None:
82 | break
83 | command = shlex.split(entry, posix=True)
84 | if not command:
85 | continue
86 | self._disable_autocomplete()
87 | history_length = readline.get_current_history_length()
88 | self.run(command)
89 | self._update_history(history_length)
90 |
91 | def _setup(self):
92 | if not self._load_data():
93 | return
94 | readline.parse_and_bind("tab: complete")
95 | self._activate_autocomplete()
96 | history_filename = os.path.join(constant.BACKSTAGE_HOME, "history")
97 | if not os.path.isdir(constant.BACKSTAGE_HOME):
98 | os.makedirs(constant.BACKSTAGE_HOME)
99 | if not os.path.isfile(history_filename):
100 | with open(history_filename, "w") as file:
101 | pass
102 | readline.read_history_file(history_filename)
103 | save_history = lambda filename: readline.write_history_file(filename)
104 | atexit.register(save_history, history_filename)
105 |
106 | def _load_data(self):
107 | tasks = util.get_tasks(self._directory)
108 | if not tasks:
109 | return False
110 | self._intro = tasks.get("")
111 | for key, val in tasks.items():
112 | if key.startswith("_") or key == "":
113 | continue
114 | if key.endswith(".help"):
115 | self._help_docs[key] = val
116 | continue
117 | if key.endswith(".test"):
118 | self._tests[key] = val
119 | continue
120 | self._tasks[key] = val
121 | return True
122 |
123 | def _wait_input(self):
124 | entry = None
125 | try:
126 | entry = input("(backstage) ")
127 | except (KeyboardInterrupt, EOFError) as e:
128 | print()
129 | return entry
130 |
131 | def _activate_autocomplete(self):
132 | words = list(self._tasks.keys())
133 | words.append("help")
134 | words = tuple(words)
135 | c = lambda text, state, words=words: complete_callback(text, state, words)
136 | readline.set_completer(c)
137 |
138 | def _disable_autocomplete(self):
139 | readline.set_completer(None)
140 |
141 | def _update_history(self, expected_length):
142 | while True:
143 | try:
144 | readline.remove_history_item(expected_length)
145 | except Exception:
146 | break
147 |
148 | def _run_task(self, name, *args, debug=False):
149 | if name.endswith(".test"):
150 | print("Please use the correct syntax to run a test.")
151 | return
152 | if name.endswith(".help"):
153 | print("Please use the correct syntax to print the help text.")
154 | return
155 | task = self._get_task(name)
156 | if not task:
157 | return
158 | report_exception = True if debug else False
159 | config = {"FailFast": False, "ReportException": report_exception,
160 | "ShowTraceback": False, "TestMode": False}
161 | self._backstage.run(task, args, config=config)
162 |
163 | def _print_intro(self, *args):
164 | intro = self._intro if self._intro else list()
165 | if intro:
166 | print("\n".join(intro).strip("- \n"))
167 |
168 | def _print_tasks_list(self, *args):
169 | keys = self._tasks.keys()
170 | results = [k for k in keys if k != "" and not k.endswith(".test")]
171 | if not results:
172 | print("No tasks available.")
173 | return
174 | n = len(results)
175 | print("Available tasks ({}):".format(n))
176 | cache = " ".join(sorted(results))
177 | cache = textwrap.wrap(cache)
178 | cache = "\n".join(cache)
179 | cache = textwrap.indent(cache, " ")
180 | print(cache)
181 |
182 | def _print_descriptive_tasks_list(self, *args):
183 | if not self._tasks:
184 | print("No tasks available.")
185 | return
186 | n = len(self._tasks)
187 | print("Available tasks ({}):\n".format(n))
188 | keys = self._tasks.keys()
189 | for key in sorted(keys):
190 | description = "No description"
191 | for line in self._help_docs.get(key + ".help", list()):
192 | if not line or line.isspace():
193 | continue
194 | description = line.strip()
195 | break
196 | print(" [{}]".format(key))
197 | print(" {}".format(description))
198 | print()
199 |
200 | def _run_tests(self, *args, debug=False):
201 | candidates = list()
202 | if args:
203 | for item in args:
204 | task = self._get_task(item)
205 | if not task:
206 | return
207 | candidates.append(task + ".test")
208 | else:
209 | for task in self._tasks.keys():
210 | if task.endswith(".test"):
211 | candidates.append(task)
212 | report_exception = True if debug else False
213 | config = {"FailFast": False, "ReportException": report_exception,
214 | "ShowTraceback": False, "TestMode": True}
215 | n = len(candidates)
216 | for i, test in enumerate(candidates):
217 | if test not in self._tests:
218 | msg = "Test skipped: '{}' doesn't exist."
219 | print(msg.format(test))
220 | else:
221 | self._backstage.run(test, config=config)
222 | if i + 1 != n:
223 | print()
224 |
225 | def _search_task(self, *args):
226 | if not args:
227 | msg = "Incomplete command. The pattern is missing."
228 | print(msg)
229 | return
230 | pattern = args[0]
231 | results = self._find_tasks_by_pattern(pattern)
232 | if not results:
233 | print("No results found.")
234 | return
235 | results = [x for x in results if x != "" and not x.endswith(".test")]
236 | n = len(results)
237 | print("Results ({}):".format(n))
238 | cache = " ".join(sorted(results))
239 | cache = textwrap.wrap(cache)
240 | cache = "\n".join(cache)
241 | cache = textwrap.indent(cache, " ")
242 | print(cache)
243 |
244 | def _search_keyword(self, *args):
245 | if not args:
246 | msg = "Incomplete command. The pattern is missing."
247 | print(msg)
248 | return
249 | keyword = args[0]
250 | results = self._find_tasks_by_keyword(keyword)
251 | if not results:
252 | print("No results found.")
253 | return
254 | results = [x for x in results if x != "" and not x.endswith(".test")]
255 | n = len(results)
256 | print("Results ({}):".format(n))
257 | cache = " ".join(sorted(results))
258 | cache = textwrap.wrap(cache)
259 | cache = "\n".join(cache)
260 | cache = textwrap.indent(cache, " ")
261 | print(cache)
262 |
263 | def _print_help_text(self, *args):
264 | if not args:
265 | print(text.INTRO)
266 | print()
267 | print(text.HELP)
268 | return
269 | task = self._get_task(args[0])
270 | if not task:
271 | return
272 | try:
273 | doc = self._help_docs[task + ".help"]
274 | except KeyError as e:
275 | msg = "Help documentation for '{}' doesn't exist."
276 | print(msg.format(task))
277 | else:
278 | print("\n".join(doc).strip("- \n"))
279 |
280 | def _get_task(self, pattern):
281 | if "*" in pattern or "?" in pattern:
282 | results = self._find_tasks_by_pattern(pattern)
283 | n = len(results)
284 | if n == 0:
285 | print("No matches.")
286 | return None
287 | if n > 1:
288 | print("Many tasks match this pattern:")
289 | cache = " ".join(sorted(results))
290 | cache = textwrap.wrap(cache)
291 | cache = "\n".join(cache)
292 | cache = textwrap.indent(cache, " ")
293 | print(cache)
294 | return None
295 | if n == 1:
296 | task = results[0]
297 | return task
298 | return None
299 | else:
300 | return pattern
301 |
302 | def _find_tasks_by_pattern(self, pattern):
303 | pattern = pattern.replace("?", r".")
304 | pattern = pattern.replace("*", r"[\S]*")
305 | results = list()
306 | for task in self._tasks.keys():
307 | if re.fullmatch(pattern, task):
308 | results.append(task)
309 | return results
310 |
311 | def _find_tasks_by_keyword(self, keyword):
312 | keyword = keyword.replace("?", r".")
313 | keyword = keyword.replace("*", r"[\S]*?")
314 | results = list()
315 | for task, body in self._help_docs.items():
316 | words = list()
317 | for line in body:
318 | for word in line.split():
319 | words.append(word)
320 | for word in words:
321 | if re.fullmatch(keyword, word, re.IGNORECASE):
322 | cache = task.split(".")
323 | del cache[-1]
324 | cache = ".".join(cache)
325 | if cache in self._tasks:
326 | results.append(cache)
327 | break
328 | return results
329 |
330 |
331 | def complete_callback(text, state, words=None):
332 | results = [w for w in words if w.startswith(text)]
333 | if state > len(results):
334 | return None
335 | return "{} ".format(results[state])
336 |
--------------------------------------------------------------------------------
/backstage/constant/.private:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrustic/backstage/89dae4a1f441e8b2828594cd6fb1a0bb513a4af7/backstage/constant/.private
--------------------------------------------------------------------------------
/backstage/constant/__init__.py:
--------------------------------------------------------------------------------
1 | import os.path
2 |
3 |
4 | BASENAME = "backstage.tasks"
5 |
6 | USER_HOME = os.path.expanduser("~")
7 |
8 | PYRUSTIC_HOME = os.path.join(USER_HOME, "PyrusticHome")
9 |
10 | BACKSTAGE_HOME = os.path.join(PYRUSTIC_HOME, "backstage")
11 |
12 | TRASH_DIR = os.path.join(PYRUSTIC_HOME, "trash")
13 |
14 | DATATYPES = ("str", "list", "dict", "int", "float")
15 |
16 | ASSIGNMENT_TAGS = ("raw", "str", "list", "dict", "int", "float",
17 | "date", "time", "dtime", "tstamp")
18 |
19 | OPERATION_TAGS = ("file", )
20 |
21 | NAMESPACES = ("L", "G", "D") # Local, Global, Database
22 |
23 | INDENT = 4
24 |
25 | BACKTICK = "`"
26 |
27 | CONFIG_OPTIONS = ("FailFast", "ReportException", "ShowTraceback", "TestMode", "AutoLineBreak")
28 |
29 | ENVIRONMENT_VARS = ("ARGS", "CWD", "DATE", "EMPTY", "ERROR", "EXCEPTION",
30 | "FALSE", "HOME", "LINE", "N", "NOW", "ONE", "OS", "OUTPUT",
31 | "R", "RANDOM", "SPACE", "STDERR", "STDIN", "STDOUT", "TASK",
32 | "TIME", "TIMEOUT", "TMP", "TRACEBACK", "TRASH", "TRUE", "ZERO")
33 |
34 | ELEMENTS = {"append": "APPEND", "assert": "ASSERT", "&": "BRANCH",
35 | "break": "BREAK", "browse": "BROWSE", "call": "CALL",
36 | "cd": "CD", "check": "CHECK", "clear": "CLEAR",
37 | "#": "COMMENT", "config": "CONFIG", "copy": "COPY",
38 | "count": "COUNT", "create": "CREATE", "default": "DEFAULT",
39 | "drop": "DROP", "elif": "ELIF", "else": "ELSE",
40 | "enter": "ENTER", "exit": "EXIT", "expose": "EXPOSE",
41 | "fail": "FAIL", "find": "FIND", "for": "FOR",
42 | "from": "FROM", "get": "GET", "if": "IF",
43 | "interface": "INTERFACE", "-": "LINE", "=": "LINE",
44 | "move": "MOVE", "pass": "PASS", "poke": "POKE",
45 | "prepend": "PREPEND", ":": "PRINT", "push": "PUSH",
46 | "read": "READ", "replace": "REPLACE", "return": "RETURN",
47 | "set": "SET", "sleep": "SLEEP", "$": "SPAWN",
48 | "split": "SPLIT", "spot": "SPOT", "store": "STORE",
49 | "~": "THREAD", "while": "WHILE", "write": "WRITE"}
50 |
--------------------------------------------------------------------------------
/backstage/core/.private:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrustic/backstage/89dae4a1f441e8b2828594cd6fb1a0bb513a4af7/backstage/core/.private
--------------------------------------------------------------------------------
/backstage/core/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import os.path
3 | import re
4 | import time
5 | import pathlib
6 | from collections import OrderedDict
7 | from backstage import error
8 | from backstage.pattern import Pattern
9 | from backstage import constant
10 | from backstage import util
11 |
12 |
13 | def run(runner, element, items=None):
14 | items = items if items else dict()
15 | handler = HANDLERS.get(element)
16 | if not handler:
17 | msg = "Unknown element '{}'.".format(element)
18 | raise error.Error(msg)
19 | handler(runner, items)
20 |
21 |
22 | def append_to_file(runner, items):
23 | variable = items.get("var")
24 | filename_var = items.get("filename_var")
25 | content = runner.get(variable)
26 | filename = runner.get(filename_var)
27 | filename = util.normpath(filename)
28 | if not os.path.isfile(filename):
29 | dirname = os.path.dirname(filename)
30 | if not os.path.isdir(dirname):
31 | os.makedirs(dirname)
32 | with open(filename, "w") as file:
33 | pass
34 | with open(filename, "a") as file:
35 | file.write(content)
36 |
37 |
38 | def assertion_handler(runner, items):
39 | result = util.eval_assertion(runner, items)
40 | runner.set("R", int(result))
41 | if not result and runner.config["TestMode"]:
42 | raise error.FailedAssertion
43 |
44 |
45 | def branch_subtask(runner, items):
46 | subtask = items.get("subtask")
47 | arguments = items.get("arguments")
48 | util.branch(runner, subtask, arguments, new_thread=False)
49 |
50 |
51 | def break_handler(runner, items):
52 | scopes = runner.scopes
53 | loop_spotted = False
54 | for scope in reversed(scopes):
55 | if scope["name"] in (Pattern.WHILE.name, Pattern.FOR.name,
56 | Pattern.FROM.name, Pattern.BROWSE.name):
57 | loop_spotted = True
58 | scope["active"] = False
59 | break
60 | if not loop_spotted:
61 | msg = "Use 'break' only to stop the execution of loops."
62 | raise error.Error(msg)
63 |
64 |
65 | def call_func(runner, items):
66 | module_name = items.get("module")
67 | callable_name = items.get("function")
68 | args = items.get("arguments")
69 | args = args.split() if args else list()
70 | module = runner.modules.get(module_name)
71 | c = util.get_callable(module, callable_name)
72 | if c is None:
73 | msg = "The callable '{}' doesn't exist.".format(callable_name)
74 | raise error.Error(msg)
75 | arguments = list()
76 | for arg in args:
77 | arg = arg.strip(" ,")
78 | if not arg:
79 | continue
80 | arguments.append(runner.get(arg))
81 | runner.set("ERROR", str())
82 | runner.set("R", str())
83 | #try:
84 | r = c(*arguments)
85 | #except Exception as e:
86 | # runner.set("ERROR", str(e))
87 | # return
88 | r = _update_return_type(r)
89 | runner.set("R", r)
90 |
91 |
92 | def change_dir(runner, items):
93 | new_dir_var = items.get("dirname_var")
94 | new_dir = runner.get(new_dir_var)
95 | new_dir = util.normpath(new_dir)
96 | if not os.path.isdir(new_dir):
97 | msg = "This path doesn't exist: {}".format(new_dir)
98 | raise error.Error(msg)
99 | os.chdir(new_dir)
100 |
101 |
102 | def check_var(runner, items):
103 | variable = items.get("var")
104 | try:
105 | content = runner.get(variable)
106 | except error.VariableError as e:
107 | runner.clear("R")
108 | else:
109 | datatype = "str"
110 | types_map = [(list, "list"), (tuple, "list"), (int, "int"),
111 | (float, "float"), (dict, "dict")]
112 | for x in types_map:
113 | if isinstance(content, x[0]):
114 | datatype = x[1]
115 | runner.set("R", datatype)
116 |
117 |
118 | def clear_var(runner, items):
119 | variables = items.get("vars", str()).split()
120 | if not variables:
121 | raise error.InterpretationError
122 | for variable in variables:
123 | runner.set(variable, str())
124 |
125 |
126 | def configure(runner, items):
127 | options = items.get("options").split()
128 | if not options:
129 | raise error.InterpretationError
130 | values = list()
131 | for option in options:
132 | cache = option.split("=", maxsplit=1)
133 | if len(cache) == 2:
134 | option, value = cache
135 | try:
136 | value = bool(int(value))
137 | except ValueError as e:
138 | msg = "The configuration option value must be 0 or 1."
139 | raise error.Error(msg)
140 | else:
141 | option, value = cache[0], None
142 | if option not in constant.CONFIG_OPTIONS:
143 | msg = "Unknown configuration option '{}'. Valid options: {}"
144 | msg = msg.format(option, " ".join(constant.CONFIG_OPTIONS))
145 | raise error.Error(msg)
146 | if value is None:
147 | value = runner.config.get(option)
148 | else:
149 | runner.config[option] = value
150 | values.append(int(value))
151 | if len(values) == 1:
152 | runner.set("R", values[0])
153 | else:
154 | runner.set("R", values)
155 |
156 |
157 | def copy_resource(runner, items):
158 | src_path_var = items.get("src_path_var")
159 | dest_path_var = items.get("dest_path_var")
160 | src = runner.get(src_path_var)
161 | dest = runner.get(dest_path_var)
162 | src = util.normpath(src)
163 | dest = util.normpath(dest)
164 | util.copyto(src, dest)
165 |
166 |
167 | def count(runner, items):
168 | element = items.get("element")
169 | variable = items.get("var")
170 | tag = items.get("tag")
171 | content_iterator = util.get_content_iterator(runner, element, variable, tag)
172 | i = 0
173 | for _ in content_iterator:
174 | i += 1
175 | runner.set("R", i)
176 |
177 |
178 | def create_resource(runner, items):
179 | element = items.get("element")
180 | path_var = items.get("path_var")
181 | path = runner.get(path_var)
182 | path = util.normpath(path)
183 | if element == "dir":
184 | if os.path.isfile(path):
185 | msg = "Cannot create this directory since a file with same name exists."
186 | raise error.Error(msg)
187 | if os.path.exists(path):
188 | return
189 | try:
190 | os.makedirs(path)
191 | except Exception:
192 | pass
193 | elif element == "file":
194 | if os.path.isdir(path):
195 | msg = "Cannot create this file since a directory with same name exists."
196 | raise error.Error(msg)
197 | if os.path.exists(path):
198 | return
199 | directory = os.path.dirname(path)
200 | try:
201 | os.makedirs(directory)
202 | except Exception:
203 | pass
204 | try:
205 | with open(path, "w") as file:
206 | pass
207 | except Exception:
208 | pass
209 |
210 |
211 | def default_var(runner, items):
212 | variables = items.get("vars")
213 | variables = variables.split() if variables else list()
214 | if not variables:
215 | raise error.InterpretationError
216 | for variable in variables:
217 | items = {"var": variable, "tag": "str",
218 | "raw": "r", "value": str()}
219 | try:
220 | runner.get(variable)
221 | except error.VariableError:
222 | set_variable(runner, items)
223 |
224 |
225 | def drop_var(runner, items):
226 | variables = items.get("vars").split()
227 | if not variables:
228 | raise error.InterpretationError
229 | for var in variables:
230 | runner.delete(var)
231 |
232 |
233 | def enter_user_data(runner, items):
234 | variable = items.get("var")
235 | if not variable:
236 | input()
237 | return
238 | text = items.get("text", str())
239 | text = util.strip_delimiters(text)
240 | text = util.interpolate(runner, text, quote=False)
241 | value = input(text)
242 | runner.set(variable, value)
243 |
244 |
245 | def exit_handler(runner, items):
246 | raise error.Exit
247 |
248 |
249 | def expose(runner, items):
250 | variables = items.get("vars").split()
251 | if not variables:
252 | raise error.InterpretationError
253 | for variable in variables:
254 | var_info = util.scan_var(variable)
255 | namespace = var_info["namespace"]
256 | var = var_info["var"]
257 | if namespace != "L" or not var.isidentifier or var.isupper():
258 | msg = "Only user-defined local variables can be exposed."
259 | raise error.Error(msg)
260 | value = runner.get(var_info)
261 | with runner.lock:
262 | runner.global_vars[var] = value
263 |
264 |
265 | def fail(runner, items):
266 | runner.fail()
267 |
268 |
269 | def find(runner, items):
270 | find_all = items.get("all")
271 | category = items.get("category")
272 | dirname_var = items.get("dirname_var")
273 | regex_var = items.get("regex_var")
274 | field = items.get("field")
275 | preposition = items.get("preposition")
276 | timestamp1_var = items.get("timestamp1_var")
277 | timestamp2_var = items.get("timestamp2_var")
278 | find_all = bool(find_all)
279 | directory = runner.get(dirname_var)
280 | regex = runner.get(regex_var) if regex_var else None
281 | timestamp1 = runner.get(timestamp1_var) if timestamp1_var else None
282 | timestamp2 = runner.get(timestamp2_var) if timestamp2_var else None
283 | directory = util.normpath(directory)
284 | resources = list()
285 | runner.clear("R")
286 | if find_all:
287 | for root, dirs, files in os.walk(directory):
288 | if category == "paths":
289 | cache = dirs + files
290 | elif category == "dirs":
291 | cache = dirs
292 | elif category == "files":
293 | cache = files
294 | else:
295 | msg = "Unknown resource category '{}'".format(category)
296 | raise error.Error(msg)
297 | for x in cache:
298 | path = os.path.join(root, x)
299 | if util.match_resource(path, regex, field, preposition, timestamp1,
300 | timestamp2):
301 | resources.append(path)
302 | else:
303 | for x in os.listdir(directory):
304 | path = os.path.join(directory, x)
305 | skip = True
306 | if os.path.isdir(path) and category in ("dirs", "paths"):
307 | skip = False
308 | elif os.path.isfile(path) and category in ("files", "paths"):
309 | skip = False
310 | if skip:
311 | continue
312 | if util.match_resource(path, regex, field, preposition, timestamp1,
313 | timestamp2):
314 | resources.append(path)
315 | runner.set("R", resources)
316 |
317 |
318 | def get_handler(runner, items):
319 | element = items.get("element")
320 | index_var = items.get("index_var")
321 | variable = items.get("var")
322 | tag = items.get("tag")
323 | index = runner.get(index_var)
324 | index = int(index)
325 | content_iterator = util.get_content_iterator(runner, element, variable, tag)
326 | if index < 0:
327 | cache = [val for val in content_iterator]
328 | try:
329 | val = cache[index]
330 | except IndexError:
331 | raise error.Error("Index error.")
332 | else:
333 | runner.set("R", val)
334 | return
335 | for i, val in enumerate(content_iterator):
336 | if i == index:
337 | runner.set("R", val)
338 | return
339 | raise error.Error("Index error.")
340 |
341 |
342 | def interface_module(runner, items):
343 | module = items.get("module")
344 | name = items.get("name")
345 | m = util.get_module(module)
346 | if not m:
347 | msg = "Module '{}' not found.".format(module)
348 | raise error.Error(msg)
349 | if name:
350 | runner.modules[name] = m
351 | return
352 | runner.modules[module] = m
353 |
354 |
355 | def move_resource(runner, items):
356 | src_path_var = items.get("src_path_var")
357 | dest_path_var = items.get("dest_path_var")
358 | src = runner.get(src_path_var)
359 | dest = runner.get(dest_path_var)
360 | src = util.normpath(src)
361 | dest = util.normpath(dest)
362 | util.moveto(src, dest)
363 |
364 |
365 | def pass_line(runner, items):
366 | pass
367 |
368 |
369 | def poke_resource(runner, items):
370 | path_var = items.get("path_var")
371 | path = runner.get(path_var)
372 | path = util.normpath(path)
373 | if not os.path.exists(path):
374 | runner.clear("R")
375 | return
376 | is_dir = is_file = 0
377 | if os.path.isfile(path):
378 | is_file = 1
379 | elif os.path.isdir(path):
380 | is_dir = 1
381 | stats = os.stat(path)
382 | attributes = ("st_size", "st_mtime", "st_ctime",
383 | "st_atime", "st_nlink", "st_uid",
384 | "st_gid", "st_mode", "st_ino", "st_dev")
385 | data = dict()
386 | for name in attributes:
387 | try:
388 | value = getattr(stats, name)
389 | except AttributeError as e:
390 | continue
391 | name = name.replace("st_", "")
392 | data[name] = value
393 | data["is_file"] = is_file
394 | data["is_dir"] = is_dir
395 | data["path"] = path
396 | runner.set("R")
397 |
398 |
399 | def prepend_file(runner, items):
400 | variable = items.get("var")
401 | filename_var = items.get("filename_var")
402 | content = runner.get(variable)
403 | filename = runner.get(filename_var)
404 | filename = util.normpath(filename)
405 | if not os.path.isfile(filename):
406 | dirname = os.path.dirname(filename)
407 | if not os.path.isdir(dirname):
408 | os.makedirs(dirname)
409 | with open(filename, "w") as file:
410 | pass
411 | with open(filename, "r") as file:
412 | lines = file.readlines()
413 | lines.insert(0, content)
414 | with open(filename, "w") as file:
415 | file.write("".join(lines))
416 |
417 |
418 | def print_text(runner, items):
419 | text = items.get("text", str())
420 | text = util.strip_delimiters(text)
421 | text = util.interpolate(runner, text, quote=False)
422 | end = str()
423 | auto_line_break = runner.config.get("AutoLineBreak")
424 | if auto_line_break:
425 | end = "\n"
426 | print(text, end=end)
427 |
428 |
429 | def push(runner, items):
430 | variables = items.get("vars").split()
431 | if not vars:
432 | raise error.InterpretationError
433 | cache = list()
434 | for var in variables:
435 | data = runner.get(var)
436 | if isinstance(data, (list, tuple)):
437 | data = "\n".join(data)
438 | cache.append(data)
439 | push_cache = "\n".join(cache)
440 | runner.push_cache = push_cache.encode("utf-8")
441 |
442 |
443 | def read_file(runner, items):
444 | index_var = items.get("index_var")
445 | filename_var = items.get("filename_var")
446 | filename = runner.get(filename_var)
447 | filename = util.normpath(filename)
448 | if not os.path.isfile(filename):
449 | msg = "File not found: {}".format(filename)
450 | raise error.Error(msg)
451 | runner.clear("R")
452 | if index_var == "*":
453 | with open(filename, "r") as file:
454 | data = file.read()
455 | runner.set("R", data)
456 | return
457 | index = runner.get(index_var)
458 | index = int(index)
459 | """
460 | if index < 0:
461 | with open(filename, "r") as file:
462 | data = file.read()
463 | lines = data.splitlines()
464 | try:
465 | line = lines[index]
466 | except IndexError:
467 | raise error.Error("Index error.")
468 | else:
469 | runner.set("R", line)
470 | return
471 | """
472 | iter_content = util.iterate_content(pathlib.Path(filename), "line")
473 | if index < 0:
474 | lines = [line for line in iter_content]
475 | try:
476 | line = lines[index]
477 | except IndexError:
478 | raise error.Error("Index error.")
479 | else:
480 | runner.set("R", line)
481 | return
482 | for i, line in enumerate(iter_content):
483 | if i == index:
484 | runner.set("R", line)
485 | return
486 | raise error.Error("Index error.")
487 |
488 |
489 | def replace_text(runner, items):
490 | regex_var = items.get("regex_var")
491 | text_var = items.get("text_var")
492 | replacement_var = items.get("replacement_var")
493 | regex = runner.get(regex_var)
494 | text = runner.get(text_var)
495 | replacement = runner.get(replacement_var)
496 | text = re.sub(regex, replacement, text)
497 | runner.set("R", text)
498 |
499 |
500 | def return_handler(runner, items):
501 | variable = items.get("var")
502 | return_value = None
503 | if variable:
504 | return_value = runner.get(variable)
505 | runner.quit(return_value)
506 |
507 |
508 | def set_variable(runner, items):
509 | variable, tag, value = items.get("var"), items.get("tag"), items.get("value")
510 | if tag and tag not in constant.ASSIGNMENT_TAGS:
511 | msg1 = "Unknown assignment tag '{}'.".format(tag)
512 | msg2 = "Valid assignment tags: {}".format(" ".join(constant.ASSIGNMENT_TAGS))
513 | msg = "{}\n{}".format(msg1, msg2)
514 | raise error.Error(msg)
515 | # update tag (default to "str")
516 | tag = tag if tag else "str"
517 | # strip out backticks
518 | value = util.strip_delimiters(value)
519 | # perform interpolation if literal is not "raw"
520 | if tag != "raw":
521 | value = util.interpolate(runner, value, quote=False)
522 | var_info = util.scan_var(variable)
523 | # cast !
524 | if var_info["access"]:
525 | if tag in ("list", "dict"):
526 | tag = "str"
527 | value = util.apply_assignment_tag(value, tag)
528 | runner.set(var_info, value)
529 |
530 |
531 | def sleep(runner, items):
532 | seconds_var = items.get("seconds_var")
533 | seconds = runner.get(seconds_var)
534 | s = float(seconds)
535 | time.sleep(s)
536 |
537 |
538 | def spawn(runner, items):
539 | """
540 | $ command arg1 arg2 ... argx
541 | """
542 | mode = items.get("mode")
543 | captured = True if mode == "($)" else False
544 | input_data = runner.push_cache
545 | runner.push_cache = None
546 | program = items.get("program")
547 | arguments = items.get("arguments", str())
548 | command = "{} {}".format(program, arguments.strip())
549 | command = util.interpolate(runner, command)
550 | if not command or command.isspace():
551 | raise error.InterpretationError
552 | stdin = stdout = stderr = None
553 | if not captured:
554 | stdin = util.get_stream(runner, "STDIN")
555 | stdout = util.get_stream(runner, "STDOUT")
556 | stderr = util.get_stream(runner, "STDERR")
557 | commands = util.check_pipeline(command)
558 | is_pipeline = True if commands else False
559 | if is_pipeline:
560 | info = util.spawn_pipeline(runner, commands, input_data,
561 | stdin, stdout, stderr, captured)
562 | else:
563 | info = util.spawn(runner, command, input_data, stdin, stdout, stderr, captured)
564 | output_str = info.output.decode("utf-8") if info.output else str()
565 | error_str = info.error.decode("utf-8") if info.error else str()
566 | return_code = (info.return_codes
567 | if is_pipeline else info.return_code)
568 | runner.set("R", return_code)
569 | runner.set("OUTPUT", output_str)
570 | runner.set("ERROR", error_str)
571 |
572 |
573 | def split_text(runner, items):
574 | text_var = items.get("text_var")
575 | regex_var = items.get("regex_var")
576 | text = runner.get(text_var)
577 | regex = runner.get(regex_var)
578 | result = re.split(regex, text)
579 | runner.set("R", result)
580 |
581 |
582 | def spot(runner, items):
583 | regex_var = items.get("regex_var")
584 | text_var = items.get("text_var")
585 | regex = runner.get(regex_var)
586 | text = runner.get(text_var)
587 | x = re.findall(regex, text)
588 | runner.set("R", len(x))
589 |
590 |
591 | def store(runner, items):
592 | variables = items.get("vars").split()
593 | if not variables:
594 | raise error.InterpretationError
595 | for variable in variables:
596 | var_info = util.scan_var(variable)
597 | namespace = var_info["namespace"]
598 | var = var_info["var"]
599 | if namespace != "L" or not var.isidentifier or var.isupper():
600 | msg = "Only user-defined local variables can be stored."
601 | raise error.Error(msg)
602 | value = runner.get(var_info)
603 | with runner.lock:
604 | runner.database_vars[var] = value
605 |
606 |
607 | def thread_handler(runner, items):
608 | subtask = items.get("subtask")
609 | arguments = items.get("arguments")
610 | util.branch(runner, subtask, arguments, new_thread=True)
611 |
612 |
613 | def write_file(runner, items):
614 | variable = items.get("var")
615 | filename_var = items.get("filename_var")
616 | content = runner.get(variable)
617 | filename = runner.get(filename_var)
618 | filename = util.normpath(filename)
619 | if not os.path.isfile(filename):
620 | dirname = os.path.dirname(filename)
621 | if not os.path.isdir(dirname):
622 | os.makedirs(dirname)
623 | with open(filename, "w") as file:
624 | pass
625 | with open(filename, "w") as file:
626 | file.write(content)
627 |
628 |
629 | HANDLERS = {Pattern.APPEND.name: append_to_file,
630 | Pattern.ASSERT.name: assertion_handler,
631 | Pattern.BRANCH.name: branch_subtask,
632 | Pattern.BREAK.name: break_handler,
633 | Pattern.CALL.name: call_func,
634 | Pattern.CD.name: change_dir,
635 | Pattern.CHECK.name: check_var,
636 | Pattern.CLEAR.name: clear_var,
637 | Pattern.CONFIG.name: configure,
638 | Pattern.COPY.name: copy_resource,
639 | Pattern.COUNT.name: count,
640 | Pattern.CREATE.name: create_resource,
641 | Pattern.DEFAULT.name: default_var,
642 | Pattern.DROP.name: drop_var,
643 | Pattern.ENTER.name: enter_user_data,
644 | Pattern.EXIT.name: exit_handler,
645 | Pattern.EXPOSE.name: expose,
646 | Pattern.FAIL.name: fail,
647 | Pattern.FIND.name: find,
648 | Pattern.GET.name: get_handler,
649 | Pattern.INTERFACE.name: interface_module,
650 | Pattern.MOVE.name: move_resource,
651 | Pattern.PASS.name: pass_line,
652 | Pattern.POKE.name: poke_resource,
653 | Pattern.PREPEND.name: prepend_file,
654 | Pattern.PRINT.name: print_text,
655 | Pattern.PUSH.name: push,
656 | Pattern.READ.name: read_file,
657 | Pattern.REPLACE.name: replace_text,
658 | Pattern.RETURN.name: return_handler,
659 | Pattern.SET.name: set_variable,
660 | Pattern.SLEEP.name: sleep,
661 | Pattern.SPAWN.name: spawn,
662 | Pattern.SPLIT.name: split_text,
663 | Pattern.SPOT.name: spot,
664 | Pattern.STORE.name: store,
665 | Pattern.THREAD.name: thread_handler,
666 | Pattern.WRITE.name: write_file}
667 |
668 |
669 | def _update_return_type(r):
670 | if r is None:
671 | return r
672 | if r is True:
673 | return 1
674 | if r is False:
675 | return 0
676 | if isinstance(r, str):
677 | return r
678 | if isinstance(r, list):
679 | return r
680 | if isinstance(r, dict):
681 | return OrderedDict(r)
682 | if isinstance(r, tuple):
683 | return list(r)
684 | if isinstance(r, int):
685 | return r
686 | if isinstance(r, float):
687 | return r
688 | msg1 = "Python functions should return one of these types: "
689 | msg2 = "str, int, float, list, dict, tuple, True, False, None"
690 | raise error.Error("".join([msg1, msg2]))
691 |
--------------------------------------------------------------------------------
/backstage/error/__init__.py:
--------------------------------------------------------------------------------
1 | """Error classes"""
2 |
3 |
4 | class Error(Exception):
5 | pass
6 |
7 |
8 | class IndentError(Error):
9 | pass
10 |
11 |
12 | class InterpretationError(Error):
13 | pass
14 |
15 |
16 | class SubprocessError(Error):
17 | pass
18 |
19 |
20 | class VariableError(Error):
21 | pass
22 |
23 |
24 | class Break(Error):
25 | pass
26 |
27 |
28 | class Fail(Error):
29 | pass
30 |
31 |
32 | class Return(Error):
33 | pass
34 |
35 |
36 | class Continue(Error):
37 | pass
38 |
39 |
40 | class FailedAssertion(Error):
41 | pass
42 |
43 |
44 | class Exit(Error):
45 | pass
46 |
--------------------------------------------------------------------------------
/backstage/namespace/.private:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrustic/backstage/89dae4a1f441e8b2828594cd6fb1a0bb513a4af7/backstage/namespace/.private
--------------------------------------------------------------------------------
/backstage/namespace/__init__.py:
--------------------------------------------------------------------------------
1 | class L:
2 | def __init__(self, runner):
3 | self._runner = runner
4 |
5 | def __getattr__(self, item):
6 | return self._runner.local_vars[item]
7 |
8 |
9 | class G:
10 | def __init__(self, runner):
11 | self._runner = runner
12 |
13 | def __getattr__(self, item):
14 | return self._runner.global_vars[item]
15 |
16 |
17 | class D:
18 | def __init__(self, runner):
19 | self._runner = runner
20 |
21 | def __getattr__(self, item):
22 | return self._runner.database_vars[item]
23 |
--------------------------------------------------------------------------------
/backstage/pattern/.private:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrustic/backstage/89dae4a1f441e8b2828594cd6fb1a0bb513a4af7/backstage/pattern/.private
--------------------------------------------------------------------------------
/backstage/pattern/__init__.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | # namespace, base, index, key
5 | VARIABLE_PATTERN = r"""\A(((?PL|G|D):)|)(?P [\S]+?)((\[(?P[0-9:-]+)\])|(\.(?P[\S]+))|)\Z"""
6 |
7 |
8 | # var1, comparison1, var2, logic, var3, comparison2, var4
9 | ASSERT_PATTERN = r"""(?P[\S]+) (?P==|!=|<=|>=|<|>|in|!in|rin|!rin|matches|!matches) (?P[\S]+)(( (?Pand|or) (?P[\S]+) (?P==|!=|<=|>=|<|>|in|!in|rin|!rin|matches|!matches) (?P[\S]+))|)[\s]*\Z"""
10 |
11 |
12 | class Pattern(Enum):
13 | # indent, var, filename_var
14 | APPEND = r"""\A(?P[\s]*)append (?P[\S]+) to (?P[\S]+)[\s]*\Z"""
15 |
16 | # indent, var1, comparison1, var2, logic, var3, comparison2, var4
17 | ASSERT = r"""\A(?P[\s]*)assert """ + ASSERT_PATTERN
18 |
19 | # indent, subtask, arguments
20 | BRANCH = r"""\A(?P[\s]*)& (?P[\S]+)([\s]+(?P[\s\S]*?)|)[\s]*\Z"""
21 |
22 | # indent
23 | BREAK = r"""\A(?P[\s]*)break[\s]*\Z"""
24 |
25 | # indent, files, dirs, dirname_var
26 | BROWSE = r"""\A(?P[\s]*)browse (((?Pfiles)|)(( and |)(?Pdirs)|)) in (?P[\S]+)[\s]*\Z"""
27 |
28 | # indent, module, function, arguments
29 | CALL = r"""\A(?P[\s]*)call (?P[\S]+)\.(?P[\S]+?)(\((?P[\s\S]*?)\)|)[\s]*\Z"""
30 |
31 | # indent, dirname_var
32 | CD = r"""\A(?P[\s]*)cd (?P[\S]+)[\s]*\Z"""
33 |
34 | # indent, var
35 | CHECK = r"""\A(?P[\s]*)check (?P[\S]+)[\s]*\Z"""
36 |
37 | # indent, vars
38 | CLEAR = r"""\A(?P[\s]*)clear (?P[\s\S]*?)[\s]*\Z"""
39 |
40 | # indent
41 | COMMENT = r"""\A(?P[\s]*)#[\s\S]*\Z"""
42 |
43 | # indent, options
44 | CONFIG = r"""\A(?P[\s]*)config (?P[\s\S]*?)[\s]*\Z"""
45 |
46 | # indent, src_path_var, dest_path_var
47 | COPY = r"""\A(?P[\s]*)copy (?P[\S]+) to (?P[\S]+)[\s]*\Z"""
48 |
49 | # indent, element, var, tag
50 | COUNT = r"""\A(?P[\s]*)count (?Pchars|items|lines) in (?P[\S]+)( \((?Pfile)\)|)[\s]*\Z"""
51 |
52 | # indent, element, path_var
53 | CREATE = r"""\A(?P[\s]*)create (?Pdir|file) (?P[\S]+)[\s]*\Z"""
54 |
55 | # indent, vars
56 | DEFAULT = r"""\A(?P[\s]*)default (?P[\s\S]*?)[\s]*\Z"""
57 |
58 | # indent, vars
59 | DROP = r"""\A(?P[\s]*)drop (?P[\s\S]*?)[\s]*\Z"""
60 |
61 | # indent, var1, comparison1, var2, logic, var3, comparison2, var4
62 | ELIF = r"""\A(?P[\s]*)elif """ + ASSERT_PATTERN
63 |
64 | # indent
65 | ELSE = r"""\A(?P[\s]*)else[\s]*\Z"""
66 |
67 | # indent, var, text
68 | ENTER = r"""\A(?P[\s]*)>( (?P[\S]+)(| : (?P[\s\S]*))|)[\s]*\Z"""
69 |
70 | # indent
71 | EXIT = r"""\A(?P[\s]*)exit[\s]*\Z"""
72 |
73 | # indent, vars
74 | EXPOSE = r"""\A(?P[\s]*)expose (?P[\s\S]*?)[\s]*\Z"""
75 |
76 | # indent
77 | FAIL = r"""\A(?P[\s]*)fail[\s]*\Z"""
78 |
79 | # indent, all, category, dirname_var, regex_var, field, preposition, timestamp1_var, timestamp2_var
80 | FIND = r"""\A(?P[\s]*)find (|(?Pall) )(?Ppaths|dirs|files) in (?P[\S]+)( matching (?P[\S]+)|)( (and |)(?Paccessed|created|modified) (?Pat|after|before|between) (?P[\S]+)( and (?P[\S]+)|)|)[\s]*\Z"""
81 |
82 | # indent, element, var, tag
83 | FOR = r"""\A(?P[\s]*)for (?Pchar|item|line) in (?P[\S]+)( \((?Pfile)\)|)[\s]*\Z"""
84 |
85 | # indent, start_var, end_var
86 | FROM = r"""\A(?P[\s]*)from (?P[\S]+) to (?P[\S]+)[\s]*\Z"""
87 |
88 | # indent, element, index_var, var, tag
89 | GET = r"""\A(?P[\s]*)get (?Pchar|item|line) (?P[\S]+) from (?P[\S]+)( \((?Pfile)\)|)[\s]*\Z"""
90 |
91 | # indent, var1, comparison1, var2, logic, var3, comparison2, var4
92 | IF = r"""\A(?P[\s]*)if """ + ASSERT_PATTERN
93 |
94 | # indent, module, name
95 | INTERFACE = r"""\A(?P[\s]*)interface with (?P[\S]+)( alias (?P[\S]+)|)[\s]*\Z"""
96 |
97 | # indent
98 | LINE = r"""\A(?P[\s]*)[= -]+[\s]*\Z"""
99 |
100 | # indent, src_path_var, dest_path_var
101 | MOVE = r"""\A(?P[\s]*)move (?P[\S]+) to (?P[\S]+)[\s]*\Z"""
102 |
103 | # indent
104 | PASS = r"""\A(?P[\s]*)pass[\s]*\Z"""
105 |
106 | # indent, path_var
107 | POKE = r"""\A(?P[\s]*)poke (?P[\S]+)[\s]*\Z"""
108 |
109 | # indent, var, filename_var
110 | PREPEND = r"""\A(?P[\s]*)prepend (?P[\S]+) to (?P[\S]+)[\s]*\Z"""
111 |
112 | # indent, text
113 | PRINT = r"""\A(?P[\s]*):(| (?P[\s\S]*))\Z"""
114 |
115 | # indent, vars
116 | PUSH = r"""\A(?P[\s]*)push (?P[\s\S]*?)[\s]*\Z"""
117 |
118 | # indent, index_var, filename_var
119 | READ = r"""\A(?P[\s]*)read (?P\*|[\S]+) from (?P[\S]+)[\s]*\Z"""
120 |
121 | # indent, regex_var, text_var, replacement_var
122 | REPLACE = r"""\A(?P[\s]*)replace (?P[\S]+) in (?P[\S]+) with (?P[\S]+)[\s]*\Z"""
123 |
124 | # indent, var
125 | RETURN = r"""\A(?P[\s]*)return( (?P[\S]+)|)[\s]*\Z"""
126 |
127 | # indent, var, tag, value
128 | SET = r"""\A(?P[\s]*)set (?P[\S]+) (|\((?Praw|str|list|dict|int|float|date|time|dtime|tstamp)\) )= (?P[\s\S]*)\Z"""
129 |
130 | # seconds_var
131 | SLEEP = r"""\A(?P[\s]*)sleep (?P[\S]+)[\s]*\Z"""
132 |
133 | # ident, mode, program, arguments
134 | #SPAWN = r"""\A(?P[\s]*)(?P\$|\(\$\)) (?P[\s\S]+?)[\s]*\Z"""
135 | SPAWN = r"""\A(?P[\s]*)(?P\$|\(\$\)) (?P[\S]+)([\s]+(?P[\s\S]*?)|)[\s]*\Z"""
136 |
137 | # indent, text_var, regex_var
138 | SPLIT = r"""\A(?P[\s]*)split (?P[\S]+) with (?P[\S]+)[\s]*\Z"""
139 |
140 | # indent, regex_var, text_var
141 | SPOT = r"""\A(?P[\s]*)spot (?P[\S]+) in (?P[\S]+)[\s]*\Z"""
142 |
143 | # indent, vars
144 | STORE = r"""\A(?P[\s]*)store (?P[\s\S]*?)[\s]*\Z"""
145 |
146 | # indent, subtask, arguments
147 | THREAD = r"""\A(?P[\s]*)~ (?P[\S]+)([\s]+(?P[\s\S]*?)|)[\s]*\Z"""
148 |
149 | # indent, var1, comparison1, var2, logic, var3, comparison2, var4
150 | WHILE = r"""\A(?P[\s]*)while """ + ASSERT_PATTERN
151 |
152 | # indent, var, filename_var
153 | WRITE = r"""\A(?P[\s]*)write (?P[\S]+) to (?P[\S]+)[\s]*\Z"""
154 |
--------------------------------------------------------------------------------
/backstage/runner/.private:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrustic/backstage/89dae4a1f441e8b2828594cd6fb1a0bb513a4af7/backstage/runner/.private
--------------------------------------------------------------------------------
/backstage/runner/__init__.py:
--------------------------------------------------------------------------------
1 | """Project Backstage API"""
2 | import os
3 | import os.path
4 | import sys
5 | import time
6 | import textwrap
7 | import traceback
8 | import random
9 | import shlex
10 | import oscan
11 | from threading import Thread
12 | from tempfile import TemporaryDirectory
13 | from backstage import error, util
14 | from backstage import core
15 | from backstage import constant
16 | from backstage.pattern import Pattern
17 | from backstage.usage import Usage
18 |
19 |
20 | class Runner:
21 | def __init__(self, backstage, task, rid, arguments=None, config=None):
22 | self._backstage = backstage
23 | self._task = task
24 | self._rid = rid
25 | if isinstance(arguments, str):
26 | arguments = shlex.split(arguments, posix=True)
27 | self._arguments = list(arguments) if arguments else list()
28 | new_config = config if config else dict()
29 | self._config = {"FailFast": False, "ReportException": False,
30 | "ShowTraceback": False, "TestMode": False,
31 | "AutoLineBreak": True}
32 | self._config.update(new_config)
33 | self._task_body = None
34 | self._tempdir = TemporaryDirectory()
35 | self._local_vars = util.create_env_vars(self._arguments, self._tempdir)
36 | self._global_vars = backstage.global_vars
37 | self._database_vars = backstage.database_vars
38 | self._lock = backstage.lock
39 | self._threads = list()
40 | self._threads_timeouts = list()
41 | self._index = 0
42 | self._active = False
43 | self._expired = False
44 | self._is_success = False
45 | self._expected_indent = None # (int_n_indents, bool_strict)
46 | default_scope = {"name": "MAIN", "index": 0, "active": True,
47 | "indents": 0, "data": None}
48 | self._scopes = list()
49 | self._scopes.append(default_scope)
50 | self._push_cache = None
51 | self._modules = dict()
52 | self._return_value = str()
53 | self._indents = 0
54 | self._cached_indents = 0
55 | self._indent_shift = 0
56 | self._setup()
57 |
58 | @property
59 | def backstage(self):
60 | return self._backstage
61 |
62 | @property
63 | def task(self):
64 | return self._task
65 |
66 | @property
67 | def rid(self):
68 | return self._rid
69 |
70 | @property
71 | def task_body(self):
72 | return self._task_body
73 |
74 | @property
75 | def arguments(self):
76 | return self._arguments
77 |
78 | @property
79 | def local_vars(self):
80 | return self._local_vars
81 |
82 | @property
83 | def global_vars(self):
84 | return self._global_vars
85 |
86 | @property
87 | def database_vars(self):
88 | return self._database_vars
89 |
90 | @property
91 | def active(self):
92 | return self._active
93 |
94 | @property
95 | def expired(self):
96 | return self._expired
97 |
98 | @property
99 | def lock(self):
100 | return self._lock
101 |
102 | @property
103 | def index(self):
104 | return self._index
105 |
106 | @index.setter
107 | def index(self, val):
108 | self._index = val
109 |
110 | @property
111 | def expected_indent(self):
112 | return self._expected_indent
113 |
114 | @expected_indent.setter
115 | def expected_indent(self, val):
116 | self._expected_indent = val
117 |
118 | @property
119 | def scope(self):
120 | return self._scopes[-1]
121 |
122 | @property
123 | def scopes(self):
124 | return self._scopes
125 |
126 | @property
127 | def push_cache(self):
128 | return self._push_cache
129 |
130 | @push_cache.setter
131 | def push_cache(self, val):
132 | self._push_cache = val
133 |
134 | @property
135 | def modules(self):
136 | return self._modules
137 |
138 | @property
139 | def config(self):
140 | return self._config
141 |
142 | @property
143 | def is_success(self):
144 | return self._is_success
145 |
146 | @property
147 | def return_value(self):
148 | return self._return_value
149 |
150 | def start(self):
151 | """
152 | task is a string
153 | """
154 | if self._active or self._expired:
155 | return False
156 | self._active = True
157 | with self._lock:
158 | self._backstage.runners.append(self)
159 | if self._task != "":
160 | cache = (self._rid, int(time.time()), None)
161 | self._backstage.execution_log.append(cache)
162 | cached_exception = None
163 | try:
164 | self._run()
165 | except Exception as e:
166 | cached_exception = e
167 | # join threads
168 | for i, thread in enumerate(self._threads):
169 | try:
170 | thread.join(self._threads_timeouts[i])
171 | except KeyboardInterrupt as e:
172 | print()
173 | with self._lock:
174 | if self._task != "":
175 | cache = (self._rid, int(time.time()), self._is_success)
176 | self._backstage.execution_log.append(cache)
177 | # cleanup
178 | self._tempdir.cleanup()
179 | self._expired = True
180 | if cached_exception:
181 | raise cached_exception
182 | return True
183 |
184 | def branch(self, subtask, arguments=None, new_thread=False):
185 | if self._expired:
186 | return False
187 | new_rid = self._backstage.gen_rid()
188 | inherited_config = self._config.copy()
189 | new_runner = Runner(self._backstage, subtask, new_rid, arguments, inherited_config)
190 | del inherited_config["AutoLineBreak"]
191 | if new_thread:
192 | thread = Thread(target=new_runner.start)
193 | self._threads.append(thread)
194 | timeout = self._local_vars["TIMEOUT"]
195 | self._threads_timeouts.append(timeout)
196 | thread.start()
197 | else:
198 | new_runner.start()
199 | self.set("R", new_runner.return_value)
200 | return True
201 |
202 | def get(self, variable):
203 | number = util.str_to_number(variable)
204 | if number is not None:
205 | return number
206 | if isinstance(variable, str):
207 | var_info = util.scan_var(variable)
208 | else:
209 | var_info = variable
210 | namespace = var_info["namespace"]
211 | var = var_info["var"]
212 | access = var_info["access"]
213 | access_spec = var_info["access_spec"]
214 | # update some env vars
215 | self.set("CWD", os.getcwd())
216 | self.set("DATE", util.get_date())
217 | self.set("NOW", int(time.time()))
218 | self.set("RANDOM", random.randint(0, 255))
219 | self.set("TIME", util.get_time())
220 | #
221 | vars_dict = dict()
222 | # global
223 | if namespace == "G":
224 | vars_dict = self._global_vars
225 | # database
226 | elif namespace == "D":
227 | vars_dict = self._database_vars
228 | # local
229 | elif namespace == "L":
230 | vars_dict = self._local_vars
231 | #
232 | try:
233 | val = vars_dict[var]
234 | except KeyError:
235 | raise error.VariableError(var)
236 | if access:
237 | return util.do_var_access(val, access, access_spec)
238 | return val
239 |
240 | def set(self, variable, value):
241 | if isinstance(variable, str):
242 | var_info = util.scan_var(variable)
243 | else:
244 | var_info = variable
245 | namespace = var_info["namespace"]
246 | var = var_info["var"]
247 | access = var_info["access"]
248 | access_spec = var_info["access_spec"]
249 | if namespace == "G":
250 | with self._lock:
251 | self._global_vars[var] = value
252 | elif namespace == "D":
253 | with self._lock:
254 | self._database_vars[var] = value
255 | elif namespace == "L":
256 | if var.isupper() and (var not in constant.ENVIRONMENT_VARS):
257 | msg = "New environment variables can't be created by the user."
258 | raise error.Error(msg)
259 | if access:
260 | access_spec = int(access_spec) if access == "index" else access_spec
261 | self._local_vars[var][access_spec] = value
262 | else:
263 | self._local_vars[var] = value
264 |
265 | def clear(self, variable):
266 | self.set(variable, None)
267 |
268 | def delete(self, variable):
269 | if isinstance(variable, str):
270 | var_info = util.scan_var(variable)
271 | else:
272 | var_info = variable
273 | namespace = var_info["namespace"]
274 | var = var_info["var"]
275 | try:
276 | if namespace == "G":
277 | with self._lock:
278 | del self._global_vars[var]
279 | elif namespace == "D":
280 | with self._lock:
281 | del self._database_vars[var]
282 | elif namespace == "L":
283 | del self._local_vars[var]
284 | except KeyError:
285 | pass
286 |
287 | def fail(self):
288 | raise error.Fail("Deliberate failure")
289 |
290 | def quit(self, return_value):
291 | self._return_value = return_value
292 | raise error.Return
293 |
294 | def _setup(self):
295 | #if not isinstance(self._arguments, str):
296 | # self._arguments = shlex.join(self._arguments)
297 | self._task_body = self._get_task_body()
298 | # add an empty comment at the end
299 | self._task_body.append("return")
300 |
301 | def _get_task_body(self):
302 | task_body = self._backstage.tasks.get(self._task)
303 | if task_body is None:
304 | msg = "There is no such task named '{}' !".format(self._task)
305 | print(msg)
306 | task_body = list()
307 | return task_body
308 |
309 | def _run(self):
310 | while True:
311 | try:
312 | line = self._task_body[self._index]
313 | except IndexError:
314 | return
315 | # update environment vars TASK and LINE
316 | self.set("TASK", self._task)
317 | self.set("LINE", self._index + 1)
318 | try:
319 | self._interpret(line)
320 | except error.Exit as e:
321 | raise e
322 | except error.FailedAssertion:
323 | line = self.get("LINE")
324 | task = self.get("TASK")
325 | msg = "FAILED ASSERTION at line {} of [{}]"
326 | print(msg.format(line, task))
327 | return
328 | except error.Return as e:
329 | self._is_success = True
330 | return
331 | except error.Continue as e:
332 | continue
333 | except KeyboardInterrupt:
334 | sys.exit()
335 | except Exception as e:
336 | error_name = e.__class__.__name__
337 | self.set("EXCEPTION", error_name)
338 | self.set("TRACEBACK", traceback.format_exc())
339 | msg = "{} at line {} of [{}] !"
340 | line_number = self._index + 1
341 | msg = msg.format(error_name, line_number, self._task)
342 | #
343 | if isinstance(e, error.InterpretationError):
344 | self._config["FailFast"] = True
345 | self._config["ReportException"] = True
346 | #
347 | if self._config["ReportException"]:
348 | print(msg)
349 | self._process_exception(e, line)
350 | if self._config["ShowTraceback"]:
351 | traceback.print_exc()
352 | if self._config["FailFast"] or isinstance(e, error.Fail):
353 | raise error.Exit
354 | #return
355 | else:
356 | if line and not line.isspace():
357 | self.clear("EXCEPTION")
358 | self.clear("TRACEBACK")
359 | self._index += 1
360 |
361 | def _interpret(self, line):
362 | if not line or line.isspace():
363 | return
364 | info = oscan.match(line, Pattern)
365 | if not info:
366 | raise error.InterpretationError
367 | element, items = info.name, info.groups_dict
368 | if element in (Pattern.COMMENT.name, Pattern.LINE.name):
369 | return
370 | self._indents = self._check_indent(items.get("indent"))
371 | if element in (Pattern.IF.name, Pattern.ELIF.name, Pattern.ELSE.name,
372 | Pattern.WHILE.name, Pattern.FOR.name, Pattern.BROWSE.name,
373 | Pattern.FROM.name):
374 | self._add_scope(element, items)
375 | return
376 | self._indent_shift = self._check_indent_shift()
377 | self._cleanup_scope()
378 | self._update_expected_indent()
379 | if not self.scope["active"]:
380 | return
381 | core.run(self, element, items)
382 |
383 | def _check_indent(self, spaces):
384 | indents = self._count_indents(spaces)
385 | if not self._expected_indent:
386 | return indents
387 | expected_indents, strict = self._expected_indent
388 | if strict and expected_indents != indents:
389 | plural = "s" if expected_indents > 1 else ""
390 | msg = "Expected {} indent{}."
391 | msg = msg.format(expected_indents, plural)
392 | raise error.IndentError(msg)
393 | if indents > expected_indents:
394 | msg = "Over-indented."
395 | raise error.IndentError(msg)
396 | return indents
397 |
398 | def _check_indent_shift(self):
399 | result = self._indents - self._cached_indents
400 | self._cached_indents = self._indents
401 | return result
402 |
403 | def _count_indents(self, spaces):
404 | spaces = len(spaces)
405 | if (spaces % constant.INDENT) != 0:
406 | raise error.IndentError
407 | return spaces // constant.INDENT
408 |
409 | def _add_scope(self, name, data):
410 | #active = False
411 | #if not self._scopes or (self._scopes and self._scopes[-1]["active"]):
412 | # data = self._compute_scope(name, data)
413 | # active = True if data else False
414 | exception = None
415 | try:
416 | data = self._compute_scope(name, data)
417 | except Exception as e:
418 | exception = e
419 | data = None
420 | active = True if data else False
421 | scope = {"name": name, "index": self._index,
422 | "indents": self._indents, "data": data,
423 | "active": active}
424 | self._scopes.append(scope)
425 | self._expected_indent = (self._indents + 1, True)
426 | if exception:
427 | raise exception
428 |
429 | def _update_expected_indent(self):
430 | if len(self._scopes) == 1:
431 | self._expected_indent = None
432 | else:
433 | self._expected_indent = self.scope["indents"] + 1, False
434 |
435 | def _cleanup_scope(self):
436 | if self._indent_shift >= 0:
437 | return
438 | for scope in reversed(self._scopes):
439 | scope_name = scope["name"]
440 | if scope_name == "MAIN":
441 | break
442 | if scope["indents"] < self._indents:
443 | continue
444 | if scope_name in (Pattern.WHILE.name, Pattern.FOR.name,
445 | Pattern.FROM.name, Pattern.BROWSE.name):
446 | self._update_loop(scope_name)
447 | self._scopes.pop()
448 |
449 | def _compute_scope(self, name, data):
450 | funcs = {Pattern.IF.name: self._compute_if_scope,
451 | Pattern.ELIF.name: self._compute_elif_scope,
452 | Pattern.ELSE.name: self._compute_else_scope,
453 | Pattern.WHILE.name: self._compute_while_scope,
454 | Pattern.FOR.name: self._compute_for_scope,
455 | Pattern.FROM.name: self._compute_from_scope,
456 | Pattern.BROWSE.name: self._compute_browse_scope}
457 | func = funcs[name]
458 | return func(data)
459 |
460 | def _compute_if_scope(self, data):
461 | result = util.eval_assertion(self, data)
462 | data = data if result else None
463 | return data
464 |
465 | def _compute_elif_scope(self, data):
466 | track = self._build_conditionals_track(Pattern.ELIF.name)
467 | if not self._is_conditionals_track_open(track):
468 | return None
469 | result = util.eval_assertion(self, data)
470 | data = data if result else None
471 | return data
472 |
473 | def _compute_else_scope(self, data):
474 | track = self._build_conditionals_track(Pattern.ELIF.name)
475 | if not self._is_conditionals_track_open(track):
476 | return None
477 | return data
478 |
479 | def _compute_while_scope(self, data):
480 | # update N
481 | n = data.get("N", -1) + 1
482 | data["N"] = n
483 | self.set("N", n)
484 | #
485 | result = util.eval_assertion(self, data)
486 | data = data if result else None
487 | return data
488 |
489 | def _compute_for_scope(self, data):
490 | # update N
491 | n = data.get("N", -1) + 1
492 | data["N"] = n
493 | self.set("N", n)
494 | #
495 | content_iterator = data.get("iterator")
496 | element = data.get("element")
497 | if not content_iterator:
498 | tag = data.get("tag")
499 | content_iterator = util.get_content_iterator(self, element,
500 | data["var"], tag)
501 | try:
502 | cache = next(content_iterator)
503 | except StopIteration as e:
504 | return None
505 | else:
506 | self.set(element, cache)
507 | data["iterator"] = content_iterator
508 | return data
509 |
510 | def _compute_from_scope(self, data):
511 | # update N
512 | n = data.get("N", -1) + 1
513 | data["N"] = n
514 | self.set("N", n)
515 | #
516 | start, end = data["start_var"], data["end_var"]
517 | if isinstance(start, str):
518 | start, end = int(self.get(start)), int(self.get(end))
519 | order = data.get("order")
520 | if not order:
521 | order = "asc" if start <= end else "desc"
522 | self.set("R", start)
523 | if order == "asc":
524 | if start > end:
525 | return None
526 | start += 1
527 | elif order == "desc":
528 | if start < end:
529 | return None
530 | start -= 1
531 | data["start_var"], data["end_var"] = start, end
532 | data["order"] = order
533 | return data
534 |
535 | def _compute_browse_scope(self, data):
536 | # update N
537 | n = data.get("N", -1) + 1
538 | data["N"] = n
539 | self.set("N", n)
540 | #
541 | files_tag = data.get("files")
542 | dirs_tag = data.get("dirs")
543 | if not files_tag and not dirs_tag:
544 | msg = "Resource to browse should be 'files' and or 'dirs'."
545 | raise error.Error(msg)
546 | dirname_var = data.get("dirname_var")
547 | dirname = util.normpath(self.get(dirname_var))
548 | content_iterator = data.get("iterator")
549 | if not content_iterator:
550 | content_iterator = os.walk(dirname)
551 | try:
552 | cache = next(content_iterator)
553 | except StopIteration as e:
554 | return None
555 | else:
556 | root_dir, directories, filenames = cache
557 | self.set("R", root_dir)
558 | if files_tag:
559 | self.set("files", filenames)
560 | if dirs_tag:
561 | self.set("dirs", directories)
562 | data["iterator"] = content_iterator
563 | return data
564 |
565 | def _update_loop(self, name):
566 | scope = self.scope
567 | if not scope["active"]:
568 | return
569 | cache = self._compute_scope(name, scope["data"])
570 | if cache is None:
571 | return
572 | scope["data"] = cache
573 | self._index = scope["index"] + 1
574 | self._expected_indent = scope["indents"] + 1, False
575 | raise error.Continue
576 |
577 | def _build_conditionals_track(self, candidate):
578 | cache = list()
579 | for scope in self._scopes:
580 | if scope["indents"] == self._indents:
581 | if scope["name"] in (Pattern.IF.name, Pattern.ELIF.name,
582 | Pattern.ELSE.name):
583 | cache.append(scope)
584 | else:
585 | cache = list()
586 | error_msg = "Conditionals should be ordered as: if... elif... else"
587 | if not cache and candidate in (Pattern.ELIF.name, Pattern.ELSE.name):
588 | raise error.InterpretationError(error_msg)
589 | if candidate in (Pattern.ELIF.name, Pattern.ELSE.name):
590 | if cache:
591 | for item in cache:
592 | if item["name"] == Pattern.ELSE.name:
593 | raise error.InterpretationError(error_msg)
594 | else:
595 | raise error.InterpretationError(error_msg)
596 | return cache
597 |
598 | def _is_conditionals_track_open(self, track):
599 | for scope in track:
600 | if scope["active"]:
601 | return False
602 | return True
603 |
604 | def _process_exception(self, e, line):
605 | if isinstance(e, error.InterpretationError):
606 | if e.args:
607 | msg = " ".join(e.args)
608 | else:
609 | msg = self._get_usage(line)
610 | if msg:
611 | print(msg)
612 | elif isinstance(e, error.IndentError):
613 | if e.args:
614 | msg = " ".join(e.args)
615 | else:
616 | msg = "Indents should be made of {} spaces."
617 | msg = msg.format(constant.INDENT)
618 | print(msg)
619 | elif isinstance(e, error.VariableError):
620 | if not e.args:
621 | return
622 | varname = e.args[0]
623 | msg = "Undefined variable: '{}'.".format(varname)
624 | print(msg)
625 | elif isinstance(e, error.SubprocessError):
626 | if e.args:
627 | msg = " ".join(e.args)
628 | else:
629 | msg = "Failed to run the subprocess command."
630 | print(msg)
631 | elif isinstance(e, error.Error):
632 | if not e.args:
633 | return
634 | print(" ".join(e.args))
635 | else:
636 | if not e.args:
637 | return
638 | print(e.args[-1])
639 |
640 | def _get_usage(self, line):
641 | try:
642 | cache = line.split()
643 | element = constant.ELEMENTS[cache[0]]
644 | except (IndexError, KeyError):
645 | elements = " ".join(constant.ELEMENTS.keys())
646 | usage = "Valid instructions: {}".format(elements)
647 | usage = "\n".join(textwrap.wrap(usage))
648 | return usage
649 | usage = None
650 | for item in Usage:
651 | if item.name == element:
652 | usage = item.value
653 | break
654 | if not usage:
655 | return
656 | usage = "\n".join(usage) if isinstance(usage, (tuple, list)) else usage
657 | return "Usage: {}".format(usage)
658 |
--------------------------------------------------------------------------------
/backstage/text/.private:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrustic/backstage/89dae4a1f441e8b2828594cd6fb1a0bb513a4af7/backstage/text/.private
--------------------------------------------------------------------------------
/backstage/text/__init__.py:
--------------------------------------------------------------------------------
1 | INTRO = """\
2 | Welcome to Pyrustic Backstage !
3 | Ultimate task automation tool for hackers.\
4 | """
5 |
6 | HELP = """\
7 | Usage:
8 | backstage
9 | backstage [ ...]
10 | backstage [ ...]
11 |
12 | Options:
13 | -i, --intro Show file introductory text
14 | -c, --check Show the list of tasks
15 | -C, --Check Show the descriptive list of tasks
16 | -d, --debug [ ...] Run task in debug mode
17 | -t, --test [ ...] Run tests
18 | -T, --Test [ ...] Run tests in debug mode
19 | -s, --search Search for a task by its name
20 | -S, --Search Search for a task by keyword
21 | -h, --help [] Show help text
22 |
23 | The string can use a glob-like syntax that allows
24 | wildcards '*' and '?'. Therefore, 'task1' is identical to 'task*'.
25 |
26 | Visit the webpage: https://github.com/pyrustic/backstage\
27 | """
28 |
--------------------------------------------------------------------------------
/backstage/usage/.private:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pyrustic/backstage/89dae4a1f441e8b2828594cd6fb1a0bb513a4af7/backstage/usage/.private
--------------------------------------------------------------------------------
/backstage/usage/__init__.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class Usage(Enum):
5 | APPEND = "append to "
6 | ASSERT = "assert (|) (==|!=|<=|>=|<|>|in|!in|rin|!rin|matches|!matches) [and|or] ..."
7 | BRANCH = "& [ ...]"
8 | BREAK = "break"
9 | BROWSE = "browse [files] [and] [dirs] in "
10 | CALL = "call .[(