Skip to content

Commit d08bc49

Browse files
committedNov 17, 2020
chore: introduce help documentation generator
Using ronn-ng
1 parent ea25583 commit d08bc49

File tree

8 files changed

+266
-9
lines changed

8 files changed

+266
-9
lines changed
 

‎.circleci/config.yml

+15-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ commands:
7878
install_sbt_unix:
7979
description: Install SBT
8080
steps:
81-
- run:
81+
- run:
8282
name: Installing sbt
8383
command: sdk install sbt 1.3.12
8484
install_node_npm:
@@ -109,6 +109,12 @@ commands:
109109
- run:
110110
name: NPM version
111111
command: npm --version
112+
generate_help:
113+
description: Generate CLI help files
114+
steps:
115+
- run:
116+
name: Run CLI help text builder
117+
command: npm run generate-help
112118

113119
jobs:
114120
regression-test:
@@ -117,9 +123,13 @@ jobs:
117123
- image: circleci/node:<< parameters.node_version >>
118124
steps:
119125
- checkout
126+
- setup_remote_docker:
127+
version: 19.03.13
128+
# docker_layer_caching: true
120129
- install_shellspec
121130
- install_deps
122131
- build_ts
132+
- generate_help
123133
- run:
124134
name: Run auth
125135
command: npm run snyk-auth
@@ -213,7 +223,11 @@ jobs:
213223
resource_class: small
214224
steps:
215225
- checkout
226+
- setup_remote_docker:
227+
version: 19.03.13
228+
# docker_layer_caching: true
216229
- install_deps
230+
- generate_help
217231
- run: sudo npm i -g semantic-release @semantic-release/exec pkg
218232
- run: sudo apt-get install -y osslsigncode
219233
- run:

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ snyk_report.html
1717
!/docker/snyk_report.css
1818
cert.pem
1919
key.pem
20+
help/commands-md
21+
help/commands-txt
22+
help/commands-man
2023

2124
# Diagnostic reports (https://nodejs.org/api/report.html)
2225
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

‎help/README.md

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# CLI Help files
2+
3+
Snyk CLI help files are generated from markdown sources in `help/commands-docs` folder.
4+
5+
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.
6+
7+
1. Markdown fragments
8+
2. Markdown documents for each command
9+
3. roff man pages
10+
4. plain text version of man page
11+
12+
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`.
13+
14+
This system improves authoring, as markdown is easier to format. It's keeping the docs consistent and exposes them through `man` command.
15+
16+
## Updating or adding help documents
17+
18+
Contact **Team Hammer** or open an issue in this repository when in doubt.
19+
20+
Keep all changes in `help/commands-docs` folder, as other folders are ignored by `.gitignore` file and are auto-generated in CI pipeline.
21+
22+
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.
23+
24+
### CLI options
25+
26+
```md
27+
- `--severity-threshold`=low|medium|high:
28+
Only report vulnerabilities of provided level or higher.
29+
```
30+
31+
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.
32+
33+
### Keyword extension
34+
35+
There is one non-standard markdown extension:
36+
37+
```md
38+
<KEYWORD>
39+
```
40+
41+
Visually, it'll get rendered as underlined text. It's used to mark a "variable". For example this command flag:
42+
43+
```md
44+
- `--sarif-file-output`=<OUTPUT_FILE_PATH>:
45+
(only in `test` command)
46+
Save test output in SARIF format directly to the <OUTPUT_FILE_PATH> file, regardless of whether or not you use the `--sarif` option.
47+
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.
48+
```
49+
50+
## Running locally
51+
52+
- have docker running
53+
- have `npm`/`npx` available
54+
55+
```
56+
$ npm run generate-help
57+
```

‎help/generator/generate-docs.sh

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
IMAGE_NAME=ronn-ng
5+
6+
if ! docker inspect --type=image $IMAGE_NAME >/dev/null 2>&1; then
7+
echo "Docker image $IMAGE_NAME not found, building..."
8+
docker build -t $IMAGE_NAME -f ./help/generator/ronn-ng.dockerfile ./help
9+
fi
10+
11+
echo "Running npx command to run help generator"
12+
RONN_COMMAND="docker run -i ronn-ng" npx ts-node ./help/generator/generator.ts

‎help/generator/generator.ts

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as path from 'path';
2+
import * as fs from 'fs';
3+
import { exec } from 'child_process';
4+
5+
const RONN_COMMAND = process.env.RONN_COMMAND || 'ronn';
6+
const COMMANDS: Record<string, { optionsFile?: string }> = {
7+
auth: {},
8+
test: {
9+
optionsFile: '_SNYK_COMMAND_OPTIONS',
10+
},
11+
monitor: {
12+
optionsFile: '_SNYK_COMMAND_OPTIONS',
13+
},
14+
container: {},
15+
iac: {},
16+
config: {},
17+
protect: {},
18+
policy: {},
19+
ignore: {},
20+
wizard: {},
21+
help: {},
22+
woof: {},
23+
};
24+
25+
const GENERATED_MARKDOWN_FOLDER = './help/commands-md';
26+
const GENERATED_MAN_FOLDER = './help/commands-man';
27+
const GENERATED_TXT_FOLDER = './help/commands-txt';
28+
29+
function execShellCommand(cmd): Promise<string> {
30+
return new Promise((resolve) => {
31+
exec(cmd, (error, stdout, stderr) => {
32+
if (error) {
33+
console.warn(error);
34+
}
35+
return resolve(stdout ? stdout : stderr);
36+
});
37+
});
38+
}
39+
40+
async function generateRoff(inputFile): Promise<string> {
41+
return await execShellCommand(
42+
`cat ${inputFile} | ${RONN_COMMAND} --roff --pipe --organization=Snyk.io`,
43+
);
44+
}
45+
46+
async function printRoff2Txt(inputFile) {
47+
return await execShellCommand(`cat ${inputFile} | ${RONN_COMMAND} -m`);
48+
}
49+
50+
async function processMarkdown(markdownDoc, commandName) {
51+
const markdownFilePath = `${GENERATED_MARKDOWN_FOLDER}/${commandName}.md`;
52+
const roffFilePath = `${GENERATED_MAN_FOLDER}/${commandName}.1`;
53+
const txtFilePath = `${GENERATED_TXT_FOLDER}/${commandName}.txt`;
54+
55+
console.info(`Generating markdown version ${commandName}.md`);
56+
fs.writeFileSync(markdownFilePath, markdownDoc);
57+
58+
console.info(`Generating roff version ${commandName}.1`);
59+
const roffDoc = await generateRoff(markdownFilePath);
60+
61+
fs.writeFileSync(roffFilePath, roffDoc);
62+
63+
console.info(`Generating txt version ${commandName}.txt`);
64+
const txtDoc = (await printRoff2Txt(markdownFilePath)) as string;
65+
66+
const formattedTxtDoc = txtDoc
67+
.replace(/(.)[\b](.)/gi, (match, firstChar, actualletter) => {
68+
if (firstChar === '_' && actualletter !== '_') {
69+
return `\x1b[4m${actualletter}\x1b[0m`;
70+
}
71+
return `\x1b[1m${actualletter}\x1b[0m`;
72+
})
73+
.split('\n')
74+
.slice(4, -4)
75+
.join('\n');
76+
console.log(formattedTxtDoc);
77+
78+
fs.writeFileSync(txtFilePath, formattedTxtDoc);
79+
}
80+
81+
async function run() {
82+
// Ensure folders exists
83+
[
84+
GENERATED_MAN_FOLDER,
85+
GENERATED_MARKDOWN_FOLDER,
86+
GENERATED_TXT_FOLDER,
87+
].forEach((path) => {
88+
if (!fs.existsSync(path)) {
89+
fs.mkdirSync(path);
90+
}
91+
});
92+
93+
const getMdFilePath = (filename: string) =>
94+
path.resolve(__dirname, `./../commands-docs/${filename}.md`);
95+
96+
const readFile = (filename: string) =>
97+
fs.readFileSync(getMdFilePath(filename), 'utf8');
98+
99+
const readFileIfExists = (filename: string) =>
100+
fs.existsSync(getMdFilePath(filename)) ? readFile(filename) : '';
101+
102+
const _snykHeader = readFile('_SNYK_COMMAND_HEADER');
103+
const _snykOptions = readFile('_SNYK_COMMAND_OPTIONS');
104+
const _snykGlobalOptions = readFile('_SNYK_GLOBAL_OPTIONS');
105+
const _environment = readFile('_ENVIRONMENT');
106+
const _examples = readFile('_EXAMPLES');
107+
const _exitCodes = readFile('_EXIT_CODES');
108+
const _notices = readFile('_NOTICES');
109+
110+
for (const [name, { optionsFile }] of Object.entries(COMMANDS)) {
111+
const commandDoc = readFile(name);
112+
113+
// Piece together a help file for each command
114+
const doc = `${commandDoc}
115+
116+
${optionsFile ? readFileIfExists(optionsFile) : ''}
117+
118+
${_snykGlobalOptions}
119+
120+
${readFileIfExists(`${name}-examples`)}
121+
122+
${_exitCodes}
123+
124+
${_environment}
125+
126+
${_notices}
127+
`;
128+
129+
await processMarkdown(doc, 'snyk-' + name);
130+
}
131+
132+
// This just slaps strings together for the global snyk help doc
133+
const globalDoc = `${_snykHeader}
134+
135+
${_snykOptions}
136+
${_snykGlobalOptions}
137+
138+
${_examples}
139+
140+
${_exitCodes}
141+
142+
${_environment}
143+
144+
${_notices}
145+
`;
146+
await processMarkdown(globalDoc, 'snyk');
147+
}
148+
run();

‎help/generator/ronn-ng.dockerfile

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM ruby
2+
3+
RUN gem install ronn-ng
4+
RUN apt-get update && apt-get install -y groff
5+
6+
ENV MANPAGER=cat
7+
8+
ENTRYPOINT ["/usr/local/bundle/bin/ronn"]

‎package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
"README.md"
1212
],
1313
"directories": {
14-
"test": "test"
14+
"lib": "src",
15+
"test": "test",
16+
"man": "help/commands-man",
17+
"doc": "help/commands-help"
1518
},
1619
"bin": {
1720
"snyk": "dist/cli/index.js"
@@ -25,6 +28,7 @@
2528
"find-circular": "npm run build && madge --circular ./dist",
2629
"format": "prettier --write '{src,test,scripts}/**/*.{js,ts}'",
2730
"prepare": "npm run build",
31+
"generate-help": "./help/generator/generate-docs.sh",
2832
"test:common": "npm run check-tests && npm run lint && node --require ts-node/register src/cli test --org=snyk",
2933
"test:acceptance": "tap test/acceptance/**/*.test.* test/acceptance/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register",
3034
"test:acceptance-windows": "tap test/acceptance/**/*.test.* -Rspec --timeout=300 --node-arg=-r --node-arg=ts-node/register",

‎src/cli/commands/help.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33

4+
const DEFAULT_HELP = 'snyk';
5+
46
export = async function help(item: string | boolean) {
5-
if (!item || item === true || typeof item !== 'string') {
6-
item = 'help';
7+
if (!item || item === true || typeof item !== 'string' || item === 'help') {
8+
item = DEFAULT_HELP;
79
}
810

911
// cleanse the filename to only contain letters
1012
// aka: /\W/g but figured this was easier to read
1113
item = item.replace(/[^a-z-]/gi, '');
1214

13-
if (!fs.existsSync(path.resolve(__dirname, '../../../help', item + '.txt'))) {
14-
item = 'help';
15+
try {
16+
const filename = path.resolve(
17+
__dirname,
18+
'../../../help/commands-txt',
19+
item === DEFAULT_HELP ? DEFAULT_HELP + '.txt' : `snyk-${item}.txt`,
20+
);
21+
return fs.readFileSync(filename, 'utf8');
22+
} catch (error) {
23+
const filename = path.resolve(
24+
__dirname,
25+
'../../../help/commands-txt',
26+
DEFAULT_HELP + '.txt',
27+
);
28+
return fs.readFileSync(filename, 'utf8');
1529
}
16-
17-
const filename = path.resolve(__dirname, '../../../help', item + '.txt');
18-
return fs.readFileSync(filename, 'utf8');
1930
};

0 commit comments

Comments
 (0)
Please sign in to comment.