Skip to content

Commit ded16dd

Browse files
committedMay 3, 2021
First commit
0 parents  commit ded16dd

17 files changed

+1171
-0
lines changed
 

‎.github/ISSUE_TEMPLATE.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## Expected Behavior
2+
3+
4+
## Actual Behavior
5+
6+
7+
## Steps to Reproduce the Problem
8+
9+
1.
10+
1.
11+
1.
12+
13+
## Specifications
14+
15+
- Version:
16+
- Platform:

‎.github/PULL_REQUEST_TEMPLATE.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fixes #<issue_number_goes_here>
2+
3+
> It's a good idea to open an issue first for discussion.
4+
5+
- [ ] Tests pass
6+
- [ ] Appropriate changes to README are included in PR

‎LICENSE

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
2+
Apache License
3+
Version 2.0, January 2004
4+
http://www.apache.org/licenses/
5+
6+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7+
8+
1. Definitions.
9+
10+
"License" shall mean the terms and conditions for use, reproduction,
11+
and distribution as defined by Sections 1 through 9 of this document.
12+
13+
"Licensor" shall mean the copyright owner or entity authorized by
14+
the copyright owner that is granting the License.
15+
16+
"Legal Entity" shall mean the union of the acting entity and all
17+
other entities that control, are controlled by, or are under common
18+
control with that entity. For the purposes of this definition,
19+
"control" means (i) the power, direct or indirect, to cause the
20+
direction or management of such entity, whether by contract or
21+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
22+
outstanding shares, or (iii) beneficial ownership of such entity.
23+
24+
"You" (or "Your") shall mean an individual or Legal Entity
25+
exercising permissions granted by this License.
26+
27+
"Source" form shall mean the preferred form for making modifications,
28+
including but not limited to software source code, documentation
29+
source, and configuration files.
30+
31+
"Object" form shall mean any form resulting from mechanical
32+
transformation or translation of a Source form, including but
33+
not limited to compiled object code, generated documentation,
34+
and conversions to other media types.
35+
36+
"Work" shall mean the work of authorship, whether in Source or
37+
Object form, made available under the License, as indicated by a
38+
copyright notice that is included in or attached to the work
39+
(an example is provided in the Appendix below).
40+
41+
"Derivative Works" shall mean any work, whether in Source or Object
42+
form, that is based on (or derived from) the Work and for which the
43+
editorial revisions, annotations, elaborations, or other modifications
44+
represent, as a whole, an original work of authorship. For the purposes
45+
of this License, Derivative Works shall not include works that remain
46+
separable from, or merely link (or bind by name) to the interfaces of,
47+
the Work and Derivative Works thereof.
48+
49+
"Contribution" shall mean any work of authorship, including
50+
the original version of the Work and any modifications or additions
51+
to that Work or Derivative Works thereof, that is intentionally
52+
submitted to Licensor for inclusion in the Work by the copyright owner
53+
or by an individual or Legal Entity authorized to submit on behalf of
54+
the copyright owner. For the purposes of this definition, "submitted"
55+
means any form of electronic, verbal, or written communication sent
56+
to the Licensor or its representatives, including but not limited to
57+
communication on electronic mailing lists, source code control systems,
58+
and issue tracking systems that are managed by, or on behalf of, the
59+
Licensor for the purpose of discussing and improving the Work, but
60+
excluding communication that is conspicuously marked or otherwise
61+
designated in writing by the copyright owner as "Not a Contribution."
62+
63+
"Contributor" shall mean Licensor and any individual or Legal Entity
64+
on behalf of whom a Contribution has been received by Licensor and
65+
subsequently incorporated within the Work.
66+
67+
2. Grant of Copyright License. Subject to the terms and conditions of
68+
this License, each Contributor hereby grants to You a perpetual,
69+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70+
copyright license to reproduce, prepare Derivative Works of,
71+
publicly display, publicly perform, sublicense, and distribute the
72+
Work and such Derivative Works in Source or Object form.
73+
74+
3. Grant of Patent License. Subject to the terms and conditions of
75+
this License, each Contributor hereby grants to You a perpetual,
76+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77+
(except as stated in this section) patent license to make, have made,
78+
use, offer to sell, sell, import, and otherwise transfer the Work,
79+
where such license applies only to those patent claims licensable
80+
by such Contributor that are necessarily infringed by their
81+
Contribution(s) alone or by combination of their Contribution(s)
82+
with the Work to which such Contribution(s) was submitted. If You
83+
institute patent litigation against any entity (including a
84+
cross-claim or counterclaim in a lawsuit) alleging that the Work
85+
or a Contribution incorporated within the Work constitutes direct
86+
or contributory patent infringement, then any patent licenses
87+
granted to You under this License for that Work shall terminate
88+
as of the date such litigation is filed.
89+
90+
4. Redistribution. You may reproduce and distribute copies of the
91+
Work or Derivative Works thereof in any medium, with or without
92+
modifications, and in Source or Object form, provided that You
93+
meet the following conditions:
94+
95+
(a) You must give any other recipients of the Work or
96+
Derivative Works a copy of this License; and
97+
98+
(b) You must cause any modified files to carry prominent notices
99+
stating that You changed the files; and
100+
101+
(c) You must retain, in the Source form of any Derivative Works
102+
that You distribute, all copyright, patent, trademark, and
103+
attribution notices from the Source form of the Work,
104+
excluding those notices that do not pertain to any part of
105+
the Derivative Works; and
106+
107+
(d) If the Work includes a "NOTICE" text file as part of its
108+
distribution, then any Derivative Works that You distribute must
109+
include a readable copy of the attribution notices contained
110+
within such NOTICE file, excluding those notices that do not
111+
pertain to any part of the Derivative Works, in at least one
112+
of the following places: within a NOTICE text file distributed
113+
as part of the Derivative Works; within the Source form or
114+
documentation, if provided along with the Derivative Works; or,
115+
within a display generated by the Derivative Works, if and
116+
wherever such third-party notices normally appear. The contents
117+
of the NOTICE file are for informational purposes only and
118+
do not modify the License. You may add Your own attribution
119+
notices within Derivative Works that You distribute, alongside
120+
or as an addendum to the NOTICE text from the Work, provided
121+
that such additional attribution notices cannot be construed
122+
as modifying the License.
123+
124+
You may add Your own copyright statement to Your modifications and
125+
may provide additional or different license terms and conditions
126+
for use, reproduction, or distribution of Your modifications, or
127+
for any such Derivative Works as a whole, provided Your use,
128+
reproduction, and distribution of the Work otherwise complies with
129+
the conditions stated in this License.
130+
131+
5. Submission of Contributions. Unless You explicitly state otherwise,
132+
any Contribution intentionally submitted for inclusion in the Work
133+
by You to the Licensor shall be under the terms and conditions of
134+
this License, without any additional terms or conditions.
135+
Notwithstanding the above, nothing herein shall supersede or modify
136+
the terms of any separate license agreement you may have executed
137+
with Licensor regarding such Contributions.
138+
139+
6. Trademarks. This License does not grant permission to use the trade
140+
names, trademarks, service marks, or product names of the Licensor,
141+
except as required for reasonable and customary use in describing the
142+
origin of the Work and reproducing the content of the NOTICE file.
143+
144+
7. Disclaimer of Warranty. Unless required by applicable law or
145+
agreed to in writing, Licensor provides the Work (and each
146+
Contributor provides its Contributions) on an "AS IS" BASIS,
147+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148+
implied, including, without limitation, any warranties or conditions
149+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150+
PARTICULAR PURPOSE. You are solely responsible for determining the
151+
appropriateness of using or redistributing the Work and assume any
152+
risks associated with Your exercise of permissions under this License.
153+
154+
8. Limitation of Liability. In no event and under no legal theory,
155+
whether in tort (including negligence), contract, or otherwise,
156+
unless required by applicable law (such as deliberate and grossly
157+
negligent acts) or agreed to in writing, shall any Contributor be
158+
liable to You for damages, including any direct, indirect, special,
159+
incidental, or consequential damages of any character arising as a
160+
result of this License or out of the use or inability to use the
161+
Work (including but not limited to damages for loss of goodwill,
162+
work stoppage, computer failure or malfunction, or any and all
163+
other commercial damages or losses), even if such Contributor
164+
has been advised of the possibility of such damages.
165+
166+
9. Accepting Warranty or Additional Liability. While redistributing
167+
the Work or Derivative Works thereof, You may choose to offer,
168+
and charge a fee for, acceptance of support, warranty, indemnity,
169+
or other liability obligations and/or rights consistent with this
170+
License. However, in accepting such obligations, You may act only
171+
on Your own behalf and on Your sole responsibility, not on behalf
172+
of any other Contributor, and only if You agree to indemnify,
173+
defend, and hold each Contributor harmless for any liability
174+
incurred by, or claims asserted against, such Contributor by reason
175+
of your accepting any such warranty or additional liability.
176+
177+
END OF TERMS AND CONDITIONS
178+
179+
APPENDIX: How to apply the Apache License to your work.
180+
181+
To apply the Apache License to your work, attach the following
182+
boilerplate notice, with the fields enclosed by brackets "[]"
183+
replaced with your own identifying information. (Don't include
184+
the brackets!) The text should be enclosed in the appropriate
185+
comment syntax for the file format. We also recommend that a
186+
file or class name and description of purpose be included on the
187+
same "printed page" as the copyright notice for easier
188+
identification within third-party archives.
189+
190+
Copyright [yyyy] [name of copyright owner]
191+
192+
Licensed under the Apache License, Version 2.0 (the "License");
193+
you may not use this file except in compliance with the License.
194+
You may obtain a copy of the License at
195+
196+
http://www.apache.org/licenses/LICENSE-2.0
197+
198+
Unless required by applicable law or agreed to in writing, software
199+
distributed under the License is distributed on an "AS IS" BASIS,
200+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201+
See the License for the specific language governing permissions and
202+
limitations under the License.

‎README.md

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# 🐚 zx
2+
3+
```js
4+
#!/usr/bin/env zx
5+
6+
await $`cat package.json | grep name`
7+
8+
let branch = await $`git branch --show-current`
9+
await $`dep deploy --branch=${branch}`
10+
11+
await Promise.all([
12+
$`sleep 1; echo 1`,
13+
$`sleep 2; echo 2`,
14+
$`sleep 3; echo 3`,
15+
])
16+
17+
await $`ssh medv.io uptime`
18+
```
19+
20+
Bash is great, but when it comes to writing scripts,
21+
people usually choose a more convenient programming languages.
22+
JavaScript is a perfect choose, but standard Node.js library
23+
requires additional hassle before using. `zx` package provides
24+
useful wrappers around `child_process` and gives sensible defaults.
25+
26+
## Install
27+
28+
```bash
29+
npm i -g zx
30+
```
31+
32+
## Documentation
33+
34+
Write your scripts in a file with `.mjs` extension in order to
35+
be able to use `await` on top level. In you prefer `.js` extension,
36+
wrap your script in something like `void async function () {...}()`.
37+
38+
Add next shebang at the beginning of your script:
39+
```bash
40+
#!/usr/bin/env zx
41+
```
42+
43+
Now you will be able to run your script as:
44+
```bash
45+
chmod +x ./script.mjs
46+
./script.mjs
47+
```
48+
49+
Or via `zx` bin:
50+
51+
```bash
52+
zx ./script.mjs
53+
```
54+
55+
Then using `zx` bin or via shebang, all `$`, `cd`, `fetch`, etc
56+
available without imports.
57+
58+
### `$`
59+
60+
Executes given string using `exec` function
61+
from `child_process` package and returns `Promise<ProcessOutput>`.
62+
63+
```js
64+
let count = parseInt(await $`ls -1 | wc -l`)
65+
console.log(`Files count: ${count}`)
66+
```
67+
68+
Example. Upload files in parallel:
69+
70+
```js
71+
let hosts = [...]
72+
await Promise.all(hosts.map(host =>
73+
$`rsync -azP ./src ${host}:/var/www`
74+
))
75+
```
76+
77+
```ts
78+
class ProcessOutput {
79+
readonly exitCode: number
80+
readonly stdout: string
81+
readonly stderr: string
82+
toString(): string
83+
}
84+
```
85+
86+
If executed program returns non-zero exit code, `ProcessOutput` will be thrown.
87+
88+
```js
89+
try {
90+
await $`exit 1`
91+
} catch (p) {
92+
console.log(`Exit code: ${p.exitCode}`)
93+
console.log(`Error: ${p.stderr}`)
94+
}
95+
```
96+
97+
### `cd`
98+
99+
Changes working directory.
100+
101+
```js
102+
cd('/tmp')
103+
await $`pwd` // outputs /tmp
104+
```
105+
106+
### `test`
107+
108+
Executes `test` command using `execSync` and returns `true` or `false`.
109+
110+
```js
111+
if (test('-f package.json')) {
112+
console.log('Yes')
113+
}
114+
```
115+
116+
This is equivalent of next bash code:
117+
118+
```bash
119+
if test -f package.json; then
120+
echo Yes;
121+
fi
122+
```
123+
124+
### `fetch`
125+
126+
This is a wrapper around [node-fetch](https://www.npmjs.com/package/node-fetch) package.
127+
```js
128+
let resp = await fetch('http://wttr.in')
129+
if (resp.ok) {
130+
console.log(await resp.text())
131+
}
132+
```
133+
134+
### `question`
135+
136+
This is a wrapper around [readline](https://nodejs.org/api/readline.html) package.
137+
138+
```ts
139+
type QuestionOptions = { choices: string[] }
140+
141+
function question(query: string, options?: QuestionOptions): Promise<string>
142+
```
143+
144+
Usage:
145+
146+
```js
147+
let username = await question('What is your username? ')
148+
let token = await question('Choose env variable: ', {
149+
choices: Object.keys(process.env)
150+
})
151+
```
152+
153+
154+
155+
### `chalk`
156+
157+
The [chalk](https://www.npmjs.com/package/chalk) package available without
158+
importing inside scripts.
159+
160+
```js
161+
console.log(chalk.blue('Hello world!'))
162+
```
163+
164+
### `fs`
165+
166+
The [fx](https://nodejs.org/api/fs.html) package available without importing
167+
inside scripts.
168+
169+
```js
170+
let content = await fs.readFile('./package.json')
171+
```
172+
173+
Promisified version imported by default. Same as if you write:
174+
175+
```js
176+
import {promises as fs} from 'fs'
177+
```
178+
179+
### `os`
180+
181+
The [os](https://nodejs.org/api/os.html) package available without importing
182+
inside scripts.
183+
184+
```js
185+
await $`cd ${os.homedir()} && mkdir example`
186+
```
187+
188+
### `$.shell`
189+
190+
Specifies what shell is used. Default is `/bin/sh`.
191+
192+
```js
193+
$.shell = '/bin/bash'
194+
```
195+
196+
### `$.verbose`
197+
198+
Specifies verbosity. Default: `true`.
199+
200+
In verbose mode prints executed commands with outputs of it. Same as
201+
`set -x` in bash.
202+
203+
### Importing
204+
205+
It's possible to use `$` and others with explicit import.
206+
207+
```js
208+
#!/usr/bin/env node
209+
import {$} from 'zx'
210+
await $`date`
211+
```
212+
213+
### Executing remote scripts
214+
215+
If arg to `zx` bin starts with `https://`, it will be downloaded and executed.
216+
217+
```bash
218+
zx https://medv.io/example-script.mjs
219+
```
220+
221+
## License
222+
223+
[Apache-2.0](LICENSE)

‎docs/code-of-conduct.md

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Code of Conduct
2+
3+
## Our Pledge
4+
5+
In the interest of fostering an open and welcoming environment, we as
6+
contributors and maintainers pledge to making participation in our project and
7+
our community a harassment-free experience for everyone, regardless of age, body
8+
size, disability, ethnicity, gender identity and expression, level of
9+
experience, education, socio-economic status, nationality, personal appearance,
10+
race, religion, or sexual identity and orientation.
11+
12+
## Our Standards
13+
14+
Examples of behavior that contributes to creating a positive environment
15+
include:
16+
17+
* Using welcoming and inclusive language
18+
* Being respectful of differing viewpoints and experiences
19+
* Gracefully accepting constructive criticism
20+
* Focusing on what is best for the community
21+
* Showing empathy towards other community members
22+
23+
Examples of unacceptable behavior by participants include:
24+
25+
* The use of sexualized language or imagery and unwelcome sexual attention or
26+
advances
27+
* Trolling, insulting/derogatory comments, and personal or political attacks
28+
* Public or private harassment
29+
* Publishing others' private information, such as a physical or electronic
30+
address, without explicit permission
31+
* Other conduct which could reasonably be considered inappropriate in a
32+
professional setting
33+
34+
## Our Responsibilities
35+
36+
Project maintainers are responsible for clarifying the standards of acceptable
37+
behavior and are expected to take appropriate and fair corrective action in
38+
response to any instances of unacceptable behavior.
39+
40+
Project maintainers have the right and responsibility to remove, edit, or reject
41+
comments, commits, code, wiki edits, issues, and other contributions that are
42+
not aligned to this Code of Conduct, or to ban temporarily or permanently any
43+
contributor for other behaviors that they deem inappropriate, threatening,
44+
offensive, or harmful.
45+
46+
## Scope
47+
48+
This Code of Conduct applies both within project spaces and in public spaces
49+
when an individual is representing the project or its community. Examples of
50+
representing a project or community include using an official project e-mail
51+
address, posting via an official social media account, or acting as an appointed
52+
representative at an online or offline event. Representation of a project may be
53+
further defined and clarified by project maintainers.
54+
55+
This Code of Conduct also applies outside the project spaces when the Project
56+
Steward has a reasonable belief that an individual's behavior may have a
57+
negative impact on the project or its community.
58+
59+
## Conflict Resolution
60+
61+
We do not believe that all conflict is bad; healthy debate and disagreement
62+
often yield positive results. However, it is never okay to be disrespectful or
63+
to engage in behavior that violates the project’s code of conduct.
64+
65+
If you see someone violating the code of conduct, you are encouraged to address
66+
the behavior directly with those involved. Many issues can be resolved quickly
67+
and easily, and this gives people more control over the outcome of their
68+
dispute. If you are unable to resolve the matter for any reason, or if the
69+
behavior is threatening or harassing, report it. We are dedicated to providing
70+
an environment where participants feel welcome and safe.
71+
72+
Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the
73+
Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to
74+
receive and address reported violations of the code of conduct. They will then
75+
work with a committee consisting of representatives from the Open Source
76+
Programs Office and the Google Open Source Strategy team. If for any reason you
77+
are uncomfortable reaching out to the Project Steward, please email
78+
opensource@google.com.
79+
80+
We will investigate every complaint, but you may not receive a direct response.
81+
We will use our discretion in determining when and how to follow up on reported
82+
incidents, which may range from not taking action to permanent expulsion from
83+
the project and project-sponsored spaces. We will notify the accused of the
84+
report and provide them an opportunity to discuss it before any action is taken.
85+
The identity of the reporter will be omitted from the details of the report
86+
supplied to the accused. In potentially harmful situations, such as ongoing
87+
harassment or threats to anyone's safety, we may take action without notice.
88+
89+
## Attribution
90+
91+
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
92+
available at
93+
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

‎docs/contributing.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# How to Contribute
2+
3+
We'd love to accept your patches and contributions to this project. There are
4+
just a few small guidelines you need to follow.
5+
6+
## Contributor License Agreement
7+
8+
Contributions to this project must be accompanied by a Contributor License
9+
Agreement. You (or your employer) retain the copyright to your contribution;
10+
this simply gives us permission to use and redistribute your contributions as
11+
part of the project. Head over to <https://cla.developers.google.com/> to see
12+
your current agreements on file or to sign a new one.
13+
14+
You generally only need to submit a CLA once, so if you've already submitted one
15+
(even if it was for a different project), you probably don't need to do it
16+
again.
17+
18+
## Code Reviews
19+
20+
All submissions, including submissions by project members, require review. We
21+
use GitHub pull requests for this purpose. Consult
22+
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23+
information on using pull requests.
24+
25+
## Community Guidelines
26+
27+
This project follows [Google's Open Source Community
28+
Guidelines](https://opensource.google/conduct/).

‎examples/backup-github.mjs

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env zx
2+
3+
// Copyright 2021 Google LLC
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// https://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
let username = await question('What is your GitHub username? ')
18+
let token = await question('Do you have GitHub token in env? ', {
19+
choices: Object.keys(process.env)
20+
})
21+
22+
let headers = {}
23+
if (process.env[token]) {
24+
headers = {
25+
Authorization: `token ${process.env[token]}`
26+
}
27+
}
28+
let res = await fetch(`https://api.github.com/users/${username}/repos`, {headers})
29+
let data = await res.json()
30+
let urls = data.map(x => x.git_url)
31+
32+
await $`mkdir -p backups`
33+
cd('./backups')
34+
35+
await Promise.all(urls.map(url => $`git clone ${url}`))

‎examples/basics.mjs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env zx
2+
3+
// Copyright 2021 Google LLC
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// https://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
await $`# Hello world!
18+
date
19+
`
20+
21+
let answer = await question('What is your name? ')
22+
await $`echo "Hello, ${answer}!"`
23+
24+
if (test('-f package.json')) {
25+
console.log('Yes')
26+
}

‎examples/cjs.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env zx
2+
3+
// Copyright 2021 Google LLC
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// https://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
// In CommonJS module we can't use top-level await,
18+
// so wrap in in async function.
19+
20+
void async function () {
21+
await $`echo "Hello, CommonJS!"`
22+
}()
23+
.catch(console.error)

‎examples/parallel.mjs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env zx
2+
3+
// Copyright 2021 Google LLC
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// https://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
await Promise.all([
18+
$`sleep 1; echo 1`,
19+
$`sleep 2; echo 2`,
20+
$`sleep 3; echo 3`,
21+
])

‎index.d.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
interface $ {
16+
(pieces: TemplateStringsArray, ...args: string[]): Promise<ProcessOutput>
17+
verbose: boolean
18+
shell: string
19+
cwd: string
20+
}
21+
22+
export const $: $
23+
24+
export function cd(path: string)
25+
26+
export function test(cmd: string): boolean
27+
28+
type QuestionOptions = { choices: string[] }
29+
30+
export function question(query: string, options?: QuestionOptions): Promise<string>
31+
32+
export class ProcessOutput {
33+
readonly exitCode: number
34+
readonly stdout: string
35+
readonly stderr: string
36+
toString(): string
37+
}

‎index.mjs

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {existsSync} from 'fs'
16+
import {exec, execSync} from 'child_process'
17+
import {promisify} from 'util'
18+
import {createInterface} from 'readline'
19+
import {default as nodeFetch} from 'node-fetch'
20+
import chalk from 'chalk'
21+
22+
export {chalk}
23+
24+
function colorize(cmd) {
25+
return cmd.replace(/^\w+\s/, substr => {
26+
return chalk.greenBright(substr)
27+
})
28+
}
29+
30+
export function $(pieces, ...args) {
31+
let __from = (new Error().stack.split('at ')[2]).trim()
32+
let cmd = pieces[0], i = 0
33+
for (; i < args.length; i++) cmd += args[i] + pieces[i + 1]
34+
for (++i; i < pieces.length; i++) cmd += pieces[i]
35+
36+
if ($.verbose) console.log('$', colorize(cmd))
37+
38+
return new Promise((resolve, reject) => {
39+
let options = {
40+
windowsHide: true,
41+
}
42+
if (typeof $.shell !== 'undefined') options.shell = $.shell
43+
if (typeof $.cwd !== 'undefined') options.cwd = $.cwd
44+
45+
let child = exec(cmd, options), stdout = '', stderr = '', combined = ''
46+
child.stdout.on('data', data => {
47+
if ($.verbose) process.stdout.write(data)
48+
stdout += data
49+
combined += data
50+
})
51+
child.stderr.on('data', data => {
52+
if ($.verbose) process.stderr.write(data)
53+
stderr += data
54+
combined += data
55+
})
56+
child.on('exit', code => {
57+
(code === 0 ? resolve : reject)(
58+
new ProcessOutput({code, stdout, stderr, combined, __from})
59+
)
60+
})
61+
})
62+
}
63+
64+
$.verbose = true
65+
$.shell = undefined
66+
$.cwd = undefined
67+
68+
export function cd(path) {
69+
if ($.verbose) console.log('$', colorize(`cd ${path}`))
70+
if (!existsSync(path)) {
71+
let __from = (new Error().stack.split('at ')[2]).trim()
72+
console.error(`cd: ${path}: No such directory`)
73+
console.error(` at ${__from}`)
74+
process.exit(1)
75+
}
76+
$.cwd = path
77+
}
78+
79+
export function test(cmd) {
80+
if ($.verbose) console.log('$', colorize(`test ${cmd}`))
81+
try {
82+
execSync(`test ${cmd}`)
83+
return true
84+
} catch (e) {
85+
return false
86+
}
87+
}
88+
89+
export async function question(query, options) {
90+
let completer = undefined
91+
if (Array.isArray(options?.choices)) {
92+
completer = function completer(line) {
93+
const completions = options.choices
94+
const hits = completions.filter((c) => c.startsWith(line))
95+
return [hits.length ? hits : completions, line]
96+
}
97+
}
98+
const rl = createInterface({
99+
input: process.stdin,
100+
output: process.stdout,
101+
completer,
102+
})
103+
const question = promisify(rl.question).bind(rl)
104+
let answer = await question(query)
105+
rl.close()
106+
return answer
107+
}
108+
109+
export async function fetch(url, init) {
110+
if ($.verbose) {
111+
if (typeof init !== 'undefined') console.log('$', colorize(`fetch ${url}`), init)
112+
else console.log('$', colorize(`fetch ${url}`))
113+
}
114+
return nodeFetch(url, init)
115+
}
116+
117+
export class ProcessOutput {
118+
#code = 0
119+
#stdout = ''
120+
#stderr = ''
121+
#combined = ''
122+
#__from = ''
123+
124+
constructor({code, stdout, stderr, combined, __from}) {
125+
this.#code = code
126+
this.#stdout = stdout
127+
this.#stderr = stderr
128+
this.#combined = combined
129+
this.#__from = __from
130+
}
131+
132+
toString() {
133+
return this.#combined.replace(/\n$/, '')
134+
}
135+
136+
get stdout() {
137+
return this.#stdout.replace(/\n$/, '')
138+
}
139+
140+
get stderr() {
141+
return this.#stderr.replace(/\n$/, '')
142+
}
143+
144+
get exitCode() {
145+
return this.#code
146+
}
147+
148+
get __from() {
149+
return this.#__from
150+
}
151+
}

‎package-lock.json

+155
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "zx",
3+
"version": "1.0.1",
4+
"description": "Replaces bash with js",
5+
"main": "index.mjs",
6+
"types": "index.d.ts",
7+
"bin": {
8+
"zx": "zx.mjs"
9+
},
10+
"scripts": {
11+
"test": "zx test.mjs"
12+
},
13+
"dependencies": {
14+
"chalk": "^4.1.1",
15+
"node-fetch": "^2.6.1",
16+
"uuid": "^8.3.2"
17+
},
18+
"repository": "google/zx",
19+
"author": "Anton Medvedev <anton@medv.io>",
20+
"license": "Apache-2.0"
21+
}

‎test.mjs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
await Promise.all([
16+
$`sleep 1; echo 1`,
17+
$`sleep 2; echo 2`,
18+
$`sleep 3; echo 3`,
19+
])

‎version.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
module.exports.version = require('./package.json').version

‎zx.mjs

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env node
2+
3+
// Copyright 2021 Google LLC
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// https://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
import {join, basename} from 'path'
18+
import os, {tmpdir} from 'os'
19+
import {promises as fs} from 'fs'
20+
import {v4 as uuid} from 'uuid'
21+
import {$, test, cd, question, fetch, chalk, ProcessOutput} from './index.mjs'
22+
import {version} from './version.js'
23+
24+
Object.assign(global, {
25+
$,
26+
cd,
27+
test,
28+
fetch,
29+
question,
30+
chalk,
31+
fs,
32+
os,
33+
})
34+
35+
try {
36+
let firstArg = process.argv[2]
37+
38+
if (['-v', '-V', '--version'].includes(firstArg)) {
39+
console.log(`zx version ${version}`)
40+
process.exit(0)
41+
}
42+
43+
if (typeof firstArg === 'undefined') {
44+
let ok = await scriptFromStdin()
45+
if (!ok) {
46+
console.log(`usage: zx <script>`)
47+
process.exit(2)
48+
}
49+
} else if (firstArg.startsWith('http://') || firstArg.startsWith('https://')) {
50+
await scriptFromHttp(firstArg)
51+
} else {
52+
await import(join(process.cwd(), firstArg))
53+
}
54+
55+
} catch (p) {
56+
if (p instanceof ProcessOutput) {
57+
console.error(' at ' + p.__from)
58+
process.exit(1)
59+
} else {
60+
throw p
61+
}
62+
}
63+
64+
async function scriptFromStdin() {
65+
let script = ''
66+
if (!process.stdin.isTTY) {
67+
process.stdin.setEncoding('utf8')
68+
for await (const chunk of process.stdin) {
69+
script += chunk
70+
}
71+
72+
if (script.length > 0) {
73+
let filepath = join(tmpdir(), uuid() + '.mjs')
74+
await writeAndImport(filepath, script)
75+
return true
76+
}
77+
}
78+
return false
79+
}
80+
81+
async function scriptFromHttp(firstArg) {
82+
let res = await fetch(firstArg)
83+
if (!res.ok) {
84+
console.error(`Error: Can't get ${firstArg}`)
85+
process.exit(1)
86+
}
87+
let script = await res.text()
88+
let filepath = join(tmpdir(), basename(firstArg))
89+
await writeAndImport(filepath, script)
90+
}
91+
92+
async function writeAndImport(filepath, script) {
93+
await fs.mkdtemp(filepath)
94+
try {
95+
await fs.writeFile(filepath, script)
96+
await import(filepath)
97+
} finally {
98+
await fs.rm(filepath)
99+
}
100+
}

0 commit comments

Comments
 (0)
Please sign in to comment.