-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: introduce help documentation generator
Using ronn-ng
- Loading branch information
Showing
8 changed files
with
266 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# CLI Help files | ||
|
||
Snyk CLI help files are generated from markdown sources in `help/commands-docs` folder. | ||
|
||
There is a simple templating system that pieces markdown sources together. Those are later transformed into a [roff (man-pages) format](<https://en.wikipedia.org/wiki/Roff_(software)>). Those are then saved as plaintext to be used by `--help` argument. | ||
|
||
1. Markdown fragments | ||
2. Markdown documents for each command | ||
3. roff man pages | ||
4. plain text version of man page | ||
|
||
Since [package.json supports specifying man files](https://docs.npmjs.com/cli/v6/configuring-npm/package-json#man), they will get exposed under `man snyk`. | ||
|
||
This system improves authoring, as markdown is easier to format. It's keeping the docs consistent and exposes them through `man` command. | ||
|
||
## Updating or adding help documents | ||
|
||
Contact **Team Hammer** or open an issue in this repository when in doubt. | ||
|
||
Keep all changes in `help/commands-docs` folder, as other folders are ignored by `.gitignore` file and are auto-generated in CI pipeline. | ||
|
||
See other documents and help files for hints on how to format arguments. Keep formatting simple, as the transformation to `roff` might have issues with complex structures. | ||
|
||
### CLI options | ||
|
||
```md | ||
- `--severity-threshold`=low|medium|high: | ||
Only report vulnerabilities of provided level or higher. | ||
``` | ||
|
||
CLI flag should be in backticks. Options (filenames, org names…) should use Keyword extension (see below) and literal options (true|false, low|medium|high…) should be typed as above. | ||
|
||
### Keyword extension | ||
|
||
There is one non-standard markdown extension: | ||
|
||
```md | ||
<KEYWORD> | ||
``` | ||
|
||
Visually, it'll get rendered as underlined text. It's used to mark a "variable". For example this command flag: | ||
|
||
```md | ||
- `--sarif-file-output`=<OUTPUT_FILE_PATH>: | ||
(only in `test` command) | ||
Save test output in SARIF format directly to the <OUTPUT_FILE_PATH> file, regardless of whether or not you use the `--sarif` option. | ||
This is especially useful if you want to display the human-readable test output via stdout and at the same time save the SARIF format output to a file. | ||
``` | ||
|
||
## Running locally | ||
|
||
- have docker running | ||
- have `npm`/`npx` available | ||
|
||
``` | ||
$ npm run generate-help | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
#!/usr/bin/env bash | ||
set -e | ||
|
||
IMAGE_NAME=ronn-ng | ||
|
||
if ! docker inspect --type=image $IMAGE_NAME >/dev/null 2>&1; then | ||
echo "Docker image $IMAGE_NAME not found, building..." | ||
docker build -t $IMAGE_NAME -f ./help/generator/ronn-ng.dockerfile ./help | ||
fi | ||
|
||
echo "Running npx command to run help generator" | ||
RONN_COMMAND="docker run -i ronn-ng" npx ts-node ./help/generator/generator.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import * as path from 'path'; | ||
import * as fs from 'fs'; | ||
import { exec } from 'child_process'; | ||
|
||
const RONN_COMMAND = process.env.RONN_COMMAND || 'ronn'; | ||
const COMMANDS: Record<string, { optionsFile?: string }> = { | ||
auth: {}, | ||
test: { | ||
optionsFile: '_SNYK_COMMAND_OPTIONS', | ||
}, | ||
monitor: { | ||
optionsFile: '_SNYK_COMMAND_OPTIONS', | ||
}, | ||
container: {}, | ||
iac: {}, | ||
config: {}, | ||
protect: {}, | ||
policy: {}, | ||
ignore: {}, | ||
wizard: {}, | ||
help: {}, | ||
woof: {}, | ||
}; | ||
|
||
const GENERATED_MARKDOWN_FOLDER = './help/commands-md'; | ||
const GENERATED_MAN_FOLDER = './help/commands-man'; | ||
const GENERATED_TXT_FOLDER = './help/commands-txt'; | ||
|
||
function execShellCommand(cmd): Promise<string> { | ||
return new Promise((resolve) => { | ||
exec(cmd, (error, stdout, stderr) => { | ||
if (error) { | ||
console.warn(error); | ||
} | ||
return resolve(stdout ? stdout : stderr); | ||
}); | ||
}); | ||
} | ||
|
||
async function generateRoff(inputFile): Promise<string> { | ||
return await execShellCommand( | ||
`cat ${inputFile} | ${RONN_COMMAND} --roff --pipe --organization=Snyk.io`, | ||
); | ||
} | ||
|
||
async function printRoff2Txt(inputFile) { | ||
return await execShellCommand(`cat ${inputFile} | ${RONN_COMMAND} -m`); | ||
} | ||
|
||
async function processMarkdown(markdownDoc, commandName) { | ||
const markdownFilePath = `${GENERATED_MARKDOWN_FOLDER}/${commandName}.md`; | ||
const roffFilePath = `${GENERATED_MAN_FOLDER}/${commandName}.1`; | ||
const txtFilePath = `${GENERATED_TXT_FOLDER}/${commandName}.txt`; | ||
|
||
console.info(`Generating markdown version ${commandName}.md`); | ||
fs.writeFileSync(markdownFilePath, markdownDoc); | ||
|
||
console.info(`Generating roff version ${commandName}.1`); | ||
const roffDoc = await generateRoff(markdownFilePath); | ||
|
||
fs.writeFileSync(roffFilePath, roffDoc); | ||
|
||
console.info(`Generating txt version ${commandName}.txt`); | ||
const txtDoc = (await printRoff2Txt(markdownFilePath)) as string; | ||
|
||
const formattedTxtDoc = txtDoc | ||
.replace(/(.)[\b](.)/gi, (match, firstChar, actualletter) => { | ||
if (firstChar === '_' && actualletter !== '_') { | ||
return `\x1b[4m${actualletter}\x1b[0m`; | ||
} | ||
return `\x1b[1m${actualletter}\x1b[0m`; | ||
}) | ||
.split('\n') | ||
.slice(4, -4) | ||
.join('\n'); | ||
console.log(formattedTxtDoc); | ||
|
||
fs.writeFileSync(txtFilePath, formattedTxtDoc); | ||
} | ||
|
||
async function run() { | ||
// Ensure folders exists | ||
[ | ||
GENERATED_MAN_FOLDER, | ||
GENERATED_MARKDOWN_FOLDER, | ||
GENERATED_TXT_FOLDER, | ||
].forEach((path) => { | ||
if (!fs.existsSync(path)) { | ||
fs.mkdirSync(path); | ||
} | ||
}); | ||
|
||
const getMdFilePath = (filename: string) => | ||
path.resolve(__dirname, `./../commands-docs/${filename}.md`); | ||
|
||
const readFile = (filename: string) => | ||
fs.readFileSync(getMdFilePath(filename), 'utf8'); | ||
|
||
const readFileIfExists = (filename: string) => | ||
fs.existsSync(getMdFilePath(filename)) ? readFile(filename) : ''; | ||
|
||
const _snykHeader = readFile('_SNYK_COMMAND_HEADER'); | ||
const _snykOptions = readFile('_SNYK_COMMAND_OPTIONS'); | ||
const _snykGlobalOptions = readFile('_SNYK_GLOBAL_OPTIONS'); | ||
const _environment = readFile('_ENVIRONMENT'); | ||
const _examples = readFile('_EXAMPLES'); | ||
const _exitCodes = readFile('_EXIT_CODES'); | ||
const _notices = readFile('_NOTICES'); | ||
|
||
for (const [name, { optionsFile }] of Object.entries(COMMANDS)) { | ||
const commandDoc = readFile(name); | ||
|
||
// Piece together a help file for each command | ||
const doc = `${commandDoc} | ||
${optionsFile ? readFileIfExists(optionsFile) : ''} | ||
${_snykGlobalOptions} | ||
${readFileIfExists(`${name}-examples`)} | ||
${_exitCodes} | ||
${_environment} | ||
${_notices} | ||
`; | ||
|
||
await processMarkdown(doc, 'snyk-' + name); | ||
} | ||
|
||
// This just slaps strings together for the global snyk help doc | ||
const globalDoc = `${_snykHeader} | ||
${_snykOptions} | ||
${_snykGlobalOptions} | ||
${_examples} | ||
${_exitCodes} | ||
${_environment} | ||
${_notices} | ||
`; | ||
await processMarkdown(globalDoc, 'snyk'); | ||
} | ||
run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
FROM ruby | ||
|
||
RUN gem install ronn-ng | ||
RUN apt-get update && apt-get install -y groff | ||
|
||
ENV MANPAGER=cat | ||
|
||
ENTRYPOINT ["/usr/local/bundle/bin/ronn"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,30 @@ | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
|
||
const DEFAULT_HELP = 'snyk'; | ||
|
||
export = async function help(item: string | boolean) { | ||
if (!item || item === true || typeof item !== 'string') { | ||
item = 'help'; | ||
if (!item || item === true || typeof item !== 'string' || item === 'help') { | ||
item = DEFAULT_HELP; | ||
} | ||
|
||
// cleanse the filename to only contain letters | ||
// aka: /\W/g but figured this was easier to read | ||
item = item.replace(/[^a-z-]/gi, ''); | ||
|
||
if (!fs.existsSync(path.resolve(__dirname, '../../../help', item + '.txt'))) { | ||
item = 'help'; | ||
try { | ||
const filename = path.resolve( | ||
__dirname, | ||
'../../../help/commands-txt', | ||
item === DEFAULT_HELP ? DEFAULT_HELP + '.txt' : `snyk-${item}.txt`, | ||
); | ||
return fs.readFileSync(filename, 'utf8'); | ||
} catch (error) { | ||
const filename = path.resolve( | ||
__dirname, | ||
'../../../help/commands-txt', | ||
DEFAULT_HELP + '.txt', | ||
); | ||
return fs.readFileSync(filename, 'utf8'); | ||
} | ||
|
||
const filename = path.resolve(__dirname, '../../../help', item + '.txt'); | ||
return fs.readFileSync(filename, 'utf8'); | ||
}; |