Commit fbf3351

Anton Medvedev <anton@medv.io>
2021-05-07 18:35:09
Add shell arguments escaping
1 parent 2782bb2
index.mjs
@@ -18,6 +18,7 @@ import {promisify} from 'util'
 import {createInterface} from 'readline'
 import {default as nodeFetch} from 'node-fetch'
 import chalk from 'chalk'
+import {default as escape} from 'shq'
 
 export {chalk}
 
@@ -37,8 +38,7 @@ function substitute(arg) {
 export function $(pieces, ...args) {
   let __from = (new Error().stack.split('at ')[2]).trim()
   let cmd = pieces[0], i = 0
-  for (; i < args.length; i++) cmd += substitute(args[i]) + pieces[i + 1]
-  for (++i; i < pieces.length; i++) cmd += pieces[i]
+  while (i < args.length) cmd += escape(substitute(args[i])) + pieces[++i]
 
   if ($.verbose) console.log('$', colorize(cmd))
 
@@ -49,7 +49,8 @@ export function $(pieces, ...args) {
     if (typeof $.shell !== 'undefined') options.shell = $.shell
     if (typeof $.cwd !== 'undefined') options.cwd = $.cwd
 
-    let child = exec(cmd, options), stdout = '', stderr = '', combined = ''
+    let child = exec('set -euo pipefail;' + cmd, options)
+    let stdout = '', stderr = '', combined = ''
     child.stdout.on('data', data => {
       if ($.verbose) process.stdout.write(data)
       stdout += data
package.json
@@ -13,6 +13,7 @@
   "dependencies": {
     "chalk": "^4.1.1",
     "node-fetch": "^2.6.1",
+    "shq": "^1.0.2",
     "uuid": "^8.3.2"
   },
   "publishConfig": {
README.md
@@ -14,14 +14,16 @@ await Promise.all([
   $`sleep 3; echo 3`,
 ])
 
-await $`ssh medv.io uptime`
+let name = 'foo bar'
+await $`mkdir /tmp/${name}`
 ```
 
 Bash is great, but when it comes to writing scripts, 
 people usually choose a more convenient programming language.
 JavaScript is a perfect choice, but standard Node.js library 
 requires additional hassle before using. `zx` package provides
-useful wrappers around `child_process` and gives sensible defaults. 
+useful wrappers around `child_process`, escapes arguments and 
+gives sensible defaults like `set -o pipefail`.
 
 ## Install
 
test.mjs
@@ -12,11 +12,59 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-let foo = await $`echo Error >&2; echo Hello`
-await $`echo ${foo} | wc`
-
-await Promise.all([
-  $`sleep 1; echo 1`,
-  $`sleep 2; echo 2`,
-  $`sleep 3; echo 3`,
-])
+function assert(cond, msg) {
+  if (cond) return
+  console.error('Assertion failed')
+  if (msg) console.error(msg)
+  process.exit(1)
+
+}
+
+{
+  let hello = await $`echo Error >&2; echo Hello`
+  let len = parseInt(await $`echo ${hello} | wc -c`)
+  assert(len === 6)
+}
+
+{
+  process.env.FOO = 'foo'
+  let foo = await $`echo $FOO`
+  assert(foo.stdout === 'foo\n')
+}
+
+{
+  let greeting = `"quota'" & pwd`
+  let {stdout} = await $`echo ${greeting}`
+  assert(stdout === greeting + '\n')
+}
+
+{
+  let foo = 'hi; ls'
+  let len = parseInt(await $`echo ${foo} | wc -l`)
+  assert(len === 1)
+}
+
+{
+  let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
+  assert((await $`echo ${bar}`).stdout.trim() === bar)
+}
+
+{
+  let name = 'foo bar'
+  try {
+    await $`mkdir /tmp/${name}`
+  } finally {
+    await fs.rmdir('/tmp/' + name)
+  }
+}
+
+{
+  let p
+  try {
+    p = await $`cat /dev/not_found | sort`
+  } catch (e) {
+    console.log('Caught an exception -> ok')
+    p = e
+  }
+  assert(p.exitCode === 1)
+}