Skip to content

Commit 9549866

Browse files
committed
feat(cli): reworked command line argument parsing to use commander
1 parent 8cf518a commit 9549866

File tree

4 files changed

+135
-141
lines changed

4 files changed

+135
-141
lines changed

packages/cli/cli.js

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,22 @@
11
#!/usr/bin/env node
22
// --log_all
33

4-
// NOTE: this will only run on Node > 6 or needs to be transpiled
5-
6-
// == JSCAD CLI interface, written by Rene K. Mueller <[email protected]>, Licensed under MIT License
7-
//
8-
// Description:
9-
// jscad <file> [-of <format>] [-o <output>]
10-
// e.g.
11-
// jscad test.jscad
12-
// jscad test.jscad -o test.stl
13-
// jscad test.jscad -o test.amf
14-
// jscad test.jscad -o test.dxf
15-
// jscad test.scad -o testFromSCAD.jscad
16-
// jscad test.scad -o test.stl
17-
// jscad test.stl -o test2.stl # reprocessed: stl -> jscad -> stl
18-
// jscad test.amf -o test2.jscad
19-
// jscad test.jscad -of amf
20-
// jscad test.jscad -of dxf
21-
// jscad test.jscad -of stl
22-
// jscad name_plate.jscad --name "Just Me" --title "CEO" -o amf test.amf
23-
//
4+
/*
5+
* Command Line Interface (CLI) for converting JSCAD designs to differnt external formats.
6+
*
7+
* Example:
8+
* node ./cli.js --help
9+
*/
2410
import fs from 'fs'
11+
import path from 'path'
12+
2513
import JSZip from 'jszip'
2614

2715
import { supportedFormats } from '@jscad/io'
2816

17+
import { parseArgs } from './src/parseArgs.js'
2918
import { generateOutputData } from './src/generateOutputData.js'
30-
import { determineOutputNameAndFormat } from './src/determineOutputNameAndFormat.js'
3119
import { writeOutput } from './src/writeOutput.js'
32-
import { parseArgs } from './src/parseArgs.js'
33-
34-
const version = '[VI]{version}[/VI]' // version is injected by rollup
35-
36-
// handle arguments (inputs, outputs, etc)
37-
const args = process.argv.splice(2)
38-
let { inputFile, inputFormat, outputFile, outputFormat, generateParts, zip, params, addMetaData, inputIsDirectory } = parseArgs(args)
39-
40-
// outputs
41-
const output = determineOutputNameAndFormat(outputFormat, outputFile, inputFile)
42-
outputFormat = output.outputFormat
43-
outputFile = output.outputFile
4420

4521
const clicolors = {
4622
red: '\u{1b}[31m',
@@ -50,10 +26,30 @@ const clicolors = {
5026
black: '\u{1b}[0m'
5127
}
5228

53-
const logFileOutput = (outputFile) => {
29+
const version = '[VI]{version}[/VI]' // version is injected by rollup
30+
31+
// handle arguments (inputs, outputs, etc)
32+
let { filepaths, outputFile, outputFormat, generateParts, zip, params, addMetaData, inputIsDirectory } = parseArgs()
33+
34+
// FIXME handle N input files
35+
let inputFile = filepaths[0]
36+
let inputFormat = path.extname(inputFile).substring(1)
37+
38+
// outputs
39+
if (!outputFile) {
40+
// create a base name from the input file
41+
const fileElements = path.parse(inputFile)
42+
fileElements.ext = '.' + outputFormat
43+
if (fileElements.ext == '.stla') fileElements.ext = '.stl'
44+
if (fileElements.ext == '.stlb') fileElements.ext = '.stl'
45+
fileElements.base = undefined
46+
outputFile = path.format(fileElements)
47+
}
48+
49+
const logFileOutput = (inputPath, outputPath) => {
5450
console.log(`${clicolors.blue}JSCAD: generating output ${clicolors.red}
55-
from: ${clicolors.green} ${inputFile} ${clicolors.red}
56-
to: ${clicolors.green} ${outputFile} ${clicolors.yellow}(${supportedFormats[outputFormat].description}) ${clicolors.black}
51+
from: ${clicolors.green} ${inputPath} ${clicolors.red}
52+
to: ${clicolors.green} ${outputPath} ${clicolors.yellow}(${supportedFormats[outputFormat].description}) ${clicolors.black}
5753
`)
5854
}
5955

@@ -81,14 +77,14 @@ generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputForm
8177
if (err) {
8278
console.error(err)
8379
} else {
84-
logFileOutput(zipFilename)
80+
logFileOutput(inputFile, zipFilename)
8581
}
8682
})
8783
})
8884
} else {
8985
for (let i = 0; i < outputData.length; i++) {
9086
const filename = outputFile.replace(/\.(\w+)$/, `-part-${i + 1}-of-${outputData.length}.$1`)
91-
logFileOutput(filename)
87+
logFileOutput(inputFile, filename)
9288
writeOutput(filename, outputData[i])
9389
}
9490
}
@@ -102,12 +98,12 @@ generateOutputData(src, params, { outputFile, outputFormat, inputFile, inputForm
10298
if (err) {
10399
console.error(err)
104100
} else {
105-
logFileOutput(zipFilename)
101+
logFileOutput(inputFile, zipFilename)
106102
}
107103
})
108104
})
109105
} else {
110-
logFileOutput(outputFile)
106+
logFileOutput(inputFile, outputFile)
111107
writeOutput(outputFile, outputData)
112108
}
113109
}

packages/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"@jscad/io": "workspace:3.0.2-alpha.0",
4242
"@jscad/io-utils": "workspace:3.0.2-alpha.0",
4343
"@jscad/modeling": "workspace:3.0.2-alpha.0",
44-
"jszip": "^3.10.1"
44+
"jszip": "^3.10.1",
45+
"commander": "^14.0.0"
4546
},
4647
"devDependencies": {
4748
"ava": "^4.3.3",

packages/cli/src/env.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import os from 'os'
2+
import process from 'process'
23

34
const version = '[VI]{version}[/VI]' // version is injected by rollup
45

56
export const env = () => {
6-
let env = 'JSCAD ' + version
7+
const nodeVersion = process.version
8+
9+
let env = 'JSCAD CLI ' + version + ', Node.js ' + nodeVersion
710
if (typeof document !== 'undefined') {
811
const w = document.defaultView
912
env = env + ' [' + w.navigator.userAgent + ']'

packages/cli/src/parseArgs.js

Lines changed: 93 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import fs from 'fs'
22
import path from 'path'
33

4+
import { Command, Option } from 'commander'
5+
46
import { loading } from '@jscad/core'
57

68
import { supportedInputExtensions, supportedOutputExtensions, supportedOutputFormats } from '@jscad/io'
@@ -9,113 +11,105 @@ import { env } from './env.js'
911

1012
const { getDesignEntryPoint } = loading
1113

12-
export const parseArgs = (args) => {
13-
const inputExtensions = supportedInputExtensions()
14-
const outputExtensions = supportedOutputExtensions()
15-
const outputFormats = supportedOutputFormats()
16-
17-
// hint: https://github.com/substack/node-optimist
18-
// https://github.com/visionmedia/commander.js
19-
if (args.length < 1) {
20-
console.log('USAGE:\n\njscad [-v]\n\n')
21-
console.log('jscad [-gp] [-z] <file> [-of <format>] [-o <output>]')
22-
console.log(`\t<file> :\tinput (Supported types: folder, .${inputExtensions.join(', .')})`)
23-
console.log(`\t<output>:\toutput (Supported types: folder, .${outputExtensions.join(', .')})`)
24-
console.log(`\t<format>:\t${outputFormats.join(', ')}`)
25-
process.exit(1)
14+
const outputFormats = supportedOutputFormats()
15+
const inputExtensions = supportedInputExtensions()
16+
17+
const isValidInputFileFormat = (input) => {
18+
if (input === undefined || input === null || !(typeof input === 'string')) {
19+
return false
20+
}
21+
return inputExtensions.reduce((acc, format) => input.toLowerCase().endsWith('.' + format) || acc, false)
22+
}
23+
24+
export const parseArgs = () => {
25+
const filepaths = [] // list of input file paths
26+
const parameters = [] // parameter values for main(parameters) and serialize(parameters)
27+
28+
/*
29+
* Setup command line arguments
30+
*/
31+
const program = new Command();
32+
program.name('cli.js')
33+
program.usage('[options] <files...> -- parameter values')
34+
program.argument('[files...]')
35+
program.addOption(new Option('-f, --output-format <format>', 'output format').choices(outputFormats).default('stla'))
36+
program.option('-p, --generate-parts', 'generate unique parts from the files', false)
37+
program.option('-z, --zip', 'zip the output file contents', false)
38+
program.option('-o, --output-file <filepath>', 'output file name (optional)')
39+
program.option('-m, --add-metadata', 'add metadata to output format', false)
40+
program.option('-v, --version', 'show version and environment information', false)
41+
program.action((args) => {
42+
// handle the provided arguments
43+
args.forEach((arg) => {
44+
try {
45+
fs.statSync(arg)
46+
filepaths.push(arg)
47+
} catch (e) {
48+
parameters.push(arg)
49+
}
50+
})
51+
})
52+
program.parse()
53+
54+
const options = program.opts();
55+
56+
// show the runtime environment if requested
57+
if (options.version) {
58+
env()
2659
}
2760

28-
let inputFile
29-
let inputFormat
30-
let outputFile
31-
let outputFormat
32-
let generateParts = false
33-
let zip = false
34-
const params = {} // parameters to feed the script if applicable
35-
let addMetaData = false // wether to add metadata to outputs or not : ie version info, timestamp etc
36-
let inputIsDirectory = false // did we pass in a folder or a file ?
37-
38-
const isValidInputFileFormat = (input) => {
39-
if (input === undefined || input === null || !(typeof input === 'string')) {
40-
return false
61+
//console.log(options)
62+
//console.log(filepaths)
63+
//console.log(parameters)
64+
65+
if (filepaths.length === 0) process.exit(1)
66+
67+
if (options.outputFile) {
68+
// check that the output file name implies a valid output format
69+
let outputFormat = path.extname(options.outputFile).substring(1)
70+
if (outputFormat === 'stl') outputFormat = 'stla'
71+
if (!outputFormats.includes(outputFormat)) {
72+
console.log('ERROR: invalid output file format <' + outputFormat + '>')
73+
process.exit(1)
4174
}
42-
return inputExtensions.reduce((acc, format) => input.toLowerCase().endsWith('.' + format) || acc, false)
75+
// check for toxic combinations
76+
if (filepaths.length !== 1) {
77+
console.log('ERROR: multiple inputs cannot be converted to a single output file.')
78+
process.exit(1)
79+
}
80+
options.outputFormat = outputFormat
81+
} else {
82+
options.outputFile = undefined
4383
}
44-
const getFileExtensionFromString = (input) => (input.substring(input.lastIndexOf('.') + 1)).toLowerCase()
45-
46-
const parseBool = (input) => input.toLowerCase() === 'true'
47-
48-
for (let i = 0; i < args.length; i++) {
49-
if (args[i] === '-of') { // -of <format>
50-
outputFormat = args[++i]
51-
} else if (args[i] === '-gp') {
52-
generateParts = true
53-
} else if (args[i] === '-z') {
54-
zip = true
55-
} else if (args[i].match(/^-o(\S.+)/)) { // -o<output>
56-
outputFile = args[i]
57-
outputFile = outputFile.replace(/^-o(\S+)$/, '$1')
58-
} else if (args[i] === '-o') { // -o <output>
59-
outputFile = args[++i]
60-
} else if (args[i] === '-add-metadata') { // -metadata true/false
61-
addMetaData = parseBool(args[++i])
62-
} else if (args[i].match(/^--(\w+)=(.*)/)) { // params for main()
63-
params[RegExp.$1] = RegExp.$2
64-
} else if (args[i].match(/^--(\w+)$/)) { // params for main()
65-
params[RegExp.$1] = args[++i]
66-
} else if (isValidInputFileFormat(args[i])) {
67-
inputFile = args[i]
68-
inputFormat = getFileExtensionFromString(args[i])
69-
if (!fs.statSync(inputFile).isFile()) {
70-
console.log('ERROR: cannot open input file/directory <' + inputFile + '>')
71-
process.exit(1)
72-
}
73-
} else if (args[i].match(/^-v$/)) { // show the version and the environment information
74-
env()
75-
} else {
76-
inputFile = args[i]
77-
if (fs.statSync(inputFile).isDirectory()) {
78-
inputIsDirectory = true
79-
// get actual design entry point if applicable (if passed a folder as input etc)
80-
inputFile = getDesignEntryPoint(fs, inputFile)
81-
if (!inputFile) {
82-
console.log('ERROR: could not determine entry point of project.')
83-
console.log('Verify main or index exists')
84-
process.exit(1)
85-
}
86-
inputFormat = path.extname(inputFile).substring(1)
87-
} else {
88-
console.log('ERROR: invalid file name or argument <' + args[i] + '>')
89-
console.log("Type 'jscad' for a list of supported types")
84+
85+
// check for use of a directory, and determine the design entry point
86+
options.inputIsDirectory = false
87+
if (filepaths.length === 1) {
88+
if (fs.statSync(filepaths[0]).isDirectory()) {
89+
options.inputIsDirectory = true
90+
// get actual design entry point
91+
const filepath = getDesignEntryPoint(fs, filepaths[0])
92+
if (!filepath) {
93+
console.log('ERROR: could not determine entry point of project <' + filepaths[0] + '>')
94+
console.log('Verify project main or index exists')
9095
process.exit(1)
9196
}
97+
filepaths[0] = filepath // use the entry point for conversion
9298
}
9399
}
94-
// exit if a input file was not provided
95-
if (!inputFile) process.exit(1)
96100

97-
if (!outputFormat && !outputFile) {
98-
outputFormat = 'stla'
99-
}
100-
if (!outputFormat && outputFile) {
101-
outputFormat = path.extname(outputFile).substring(1)
102-
if (outputFormat === 'stl') outputFormat = 'stla'
103-
}
104-
if (!outputFormats.includes(outputFormat)) {
105-
console.log('ERROR: invalid output format <' + outputFormat + '>')
106-
console.log("Type 'jscad' for a list of supported types")
107-
process.exit(1)
108-
}
101+
// check that all input files are valid formats for conversion
102+
filepaths.forEach((filepath) => {
103+
if (!isValidInputFileFormat(filepath)) {
104+
console.log('ERROR: invalid input file format <' + filepath + '>')
105+
process.exit(1)
106+
}
107+
})
109108

110-
return {
111-
inputFile,
112-
inputFormat,
113-
outputFile,
114-
outputFormat,
115-
generateParts,
116-
zip,
117-
params,
118-
addMetaData,
119-
inputIsDirectory
120-
}
109+
options.filepaths = filepaths
110+
options.params = parameters
111+
112+
//console.log('RETURN',options)
113+
114+
return options
121115
}

0 commit comments

Comments
 (0)