GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - un-ts/sh-syntax: A WASM shell parser and formatter with bash support, based on `mvdan/sh`

BranchesTags
Open more actions menu

Latest commit

 

History

121 Commits
121 Commits

Folders and files

NameName
Last commit message
Last commit date
.changeset
.changeset
 
 
.github/workflows
.github/workflows
 
 
.yarn
.yarn
 
 
benchmark
benchmark
 
 
processor
processor
 
 
scripts/gen-encoder
scripts/gen-encoder
 
 
shell @ 190ed80
shell @ 190ed80
 
 
src
src
 
 
test
test
 
 
vendors
vendors
 
 
.commitlintrc
.commitlintrc
 
 
.editorconfig
.editorconfig
 
 
.gitattributes
.gitattributes
 
 
.gitignore
.gitignore
 
 
.gitmodules
.gitmodules
 
 
.nano-staged.js
.nano-staged.js
 
 
.nvmrc
.nvmrc
 
 
.oxfmtignore
.oxfmtignore
 
 
.prettierignore
.prettierignore
 
 
.prettierrc
.prettierrc
 
 
.remarkrc
.remarkrc
 
 
.renovaterc
.renovaterc
 
 
.simple-git-hooks.js
.simple-git-hooks.js
 
 
.yarnrc.yml
.yarnrc.yml
 
 
AGENTS.md
AGENTS.md
 
 
CHANGELOG.md
CHANGELOG.md
 
 
LICENSE
LICENSE
 
 
README.md
README.md
 
 
eslint.config.js
eslint.config.js
 
 
go.mod
go.mod
 
 
go.sum
go.sum
 
 
main.go
main.go
 
 
main.wasm
main.wasm
 
 
oxfmt.config.ts
oxfmt.config.ts
 
 
oxlint.config.ts
oxlint.config.ts
 
 
package.json
package.json
 
 
tsconfig.json
tsconfig.json
 
 
tsdown.config.ts
tsdown.config.ts
 
 
vitest.config.ts
vitest.config.ts
 
 
yarn.lock
yarn.lock
 
 

Repository files navigation

sh-syntax

GitHub Actions Workflow Status Codecov type-coverage CodeRabbit Pull Request Reviews npm GitHub Release

Conventional Commits Renovate enabled JavaScript Style Guide Code Style: Prettier changesets

A WASM shell parser and formatter with bash support, based on mvdan/sh

TOC

  • Usage
    • Install
    • API
      • node
      • source offsets
      • browser
  • Benchmark
  • Sponsors
  • Backers
  • Changelog
  • License

Usage

Install

# yarn
yarn add sh-syntax

# npm
npm i sh-syntax

API

node

import { init, parse, print } from 'sh-syntax'

// Optional: load and initialize the WASM runtime up front.
await init()

const text = "echo 'Hello World!'"
const ast = await parse(text)

// print() formats source text; printing an edited AST is a planned follow-up.
const formatted = await print(text)

// Compatibility: print(ast, { originalText }) is accepted, but the AST is
// ignored and originalText is formatted.
const legacyFormatted = await print(ast, { originalText: text })

The synchronous API is available as parseSync and printSync:

import { parseSync, printSync } from 'sh-syntax'

const ast = parseSync("echo 'Hello World!'")
const formatted = printSync("echo 'Hello World!'")

syntax tree

parse returns the complete shell syntax tree as plain, JSON-friendly data. Every node has a type plus start/end byte offsets, and fields use lowerCamelCase. Position fields are { offset, line, column } data:

import { LangVariant, parseSync, walk } from 'sh-syntax'

const ast = parseSync("echo 'Hello World!'", {
  keepComments: true,
  variant: LangVariant.LangBash,
  filepath: 'script.sh',
})

// A function visitor is the enter phase.
walk(ast, (node, parent, prop, index) => {
  if (node.type === 'Lit') {
    console.log(node.value)
  }
})

// An object visitor can pair enter and exit; exit is required.
walk(ast, {
  enter(node, parent, prop, index) {
    // ...
  },
  exit(node, parent, prop, index) {
    // ...
  },
})

const command = ast.stmts[0].cmd
if (command?.type === 'CallExpr') {
  console.log(command.args[0].start) // 0
}

Node start/end and position fields use UTF-8 byte offsets; position fields expose offset, line (1-based), and column (0-based). walk passes the nearest enclosing node, the key in the nearest enclosing object (which may be a non-node wrapper), and the array index; parent, prop, and index are null when absent. Returning false from enter skips a node's children, while false from exit stops the walk.

source offsets

AST offsets are UTF-8 byte offsets, while JavaScript strings use UTF-16 code units. Build a converter once for the source text:

import { createSourceIndex, parseSync } from 'sh-syntax'

const text = 'echo "héllo 🎉" $var\n'
const sourceIndex = createSourceIndex(text)
const ast = parseSync(text)

const word = ast.stmts[0].cmd.args.at(-1)
const start = sourceIndex(word.start)
const end = sourceIndex(word.end)

console.log(text.slice(start, end)) // $var

Offsets inside a multi-byte character map to the start of that character, and offsets outside the source are clamped to its bounds.

browser

The browser entry uses the same init() and async API as Node. The entry has no top-level await, so importing it never blocks; call init() once when convenient, or let the first parse/print call initialize the runtime:

import { init, parse, print } from 'sh-syntax'

await init()

const text = "echo 'Hello World!'"
const ast = await parse(text)
const formatted = await print(text)

The bundler must emit main.wasm as an asset (for example, Vite handles new URL('../main.wasm', import.meta.url)); Vite library mode instead inlines new URL() assets as base64, so libraries should supply their own loader:

import { getProcessor } from 'sh-syntax'

const processor = getProcessor(() =>
  fetch('https://cdn.jsdelivr.net/npm/sh-syntax@latest/main.wasm'),
)

workerd

The workerd export imports the bundled main.wasm as a compiled module, so Worker code does not call getProcessor or fetch the asset:

import { init, parse } from 'sh-syntax'

export default {
  async fetch() {
    await init()
    return new Response(JSON.stringify(await parse("echo 'Hello World!'")))
  },
}

Call init() inside the request handler: Go startup uses crypto.getRandomValues, which Workers disallow outside a request context.

Wrangler and @cloudflare/vite-plugin already resolve .wasm imports as WebAssembly.Module. Other bundlers must do the same for the workerd entry; a URL or instantiated module is not sufficient. Nitro's unwasm turns a plain .wasm import into instantiated exports, so it needs a loader that returns a WebAssembly.Module.

For TypeScript Worker builds, select the runtime declaration:

{ "compilerOptions": { "customConditions": ["workerd"] } }

Benchmark

clk: ~3.03 GHz
cpu: Apple M1 Max
runtime: node 26.12.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
sh-syntax                     18.35 ms/iter  20.02 ms               █
                      (14.59 ms … 22.44 ms)  22.20 ms ▇▇   ▂  ▇▂▂▇ ▂█▂ ▂  ▂
                    ( 94.64 kb … 608.02 kb) 577.27 kb ██▆▆▁█▁▁████▆███▁█▁▁█
                  4.26 ipc (  0.57% stalls)    NaN% L1 data cache
         58.65M cycles 249.90M instructions   0.00% retired LD/ST (   0.00)

sh-syntax (sync)              18.70 ms/iter  20.53 ms                █  ██
                      (14.52 ms … 21.81 ms)  21.48 ms █ █   ███   ██ █ ████
                    (598.83 kb … 692.00 kb) 603.65 kb █▁█▁█▁███████████████
                  4.23 ipc (  0.55% stalls)    NaN% L1 data cache
         58.66M cycles 248.24M instructions   0.00% retired LD/ST (   0.00)

sh-syntax (synckit)           18.48 ms/iter  19.62 ms         ▂ ▂   █     ▂
                      (15.49 ms … 20.98 ms)  20.57 ms   ▅    ▅█ █▅ ▅█▅  ▅▅█
                    (291.08 kb …   1.48 mb) 325.51 kb ▇▇█▇▁▁▇██▁██▇███▇▇███
                  2.80 ipc (  9.87% stalls)    NaN% L1 data cache
        497.94k cycles   1.39M instructions   0.00% retired LD/ST (   0.00)

mvdan-sh                      67.72 ms/iter  65.80 ms     █
                      (61.50 ms … 88.60 ms)  82.67 ms █ █ █
                    (  4.46 mb …  22.87 mb)  16.25 mb █████▁█▁▁▁▁▁▁▁▁▁▁▁▁▁█
                  5.38 ipc (  1.20% stalls)    NaN% L1 data cache
        210.36M cycles   1.13G instructions   0.00% retired LD/ST (   0.00)

summary
  sh-syntax
   1.01x faster than sh-syntax (synckit)
   1.02x faster than sh-syntax (sync)
   3.69x faster than mvdan-sh

See benchmark for more details.

You can try it with running node benchmark by yourself. Here is the benchmark source code.

Sponsors and Backers

Sponsors and Backers

Sponsors

1stG RxTS UnTS
1stG Open Collective sponsors RxTS Open Collective sponsors UnTS Open Collective sponsors

Backers

1stG RxTS UnTS
1stG Open Collective backers RxTS Open Collective backers UnTS Open Collective backers

Changelog

Detailed changes for each release are documented in CHANGELOG.md.

License

MIT © JounQin@1stG.me

About

A WASM shell parser and formatter with bash support, based on `mvdan/sh`

npmx.dev/sh-syntax

Topics

gogolangshellshell-scriptshellscripttinygowasm

Resources

Readme
MIT license

Security policy

Security policy
Activity
Custom properties

Stars

72 stars

Watchers

0 watching

Forks

8 forks
Report repository

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

Generated from un-ts/lib-boilerplate
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.