@intecoag/inteco-cli 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/modules/extdSearch.js +63 -178
- package/src/modules/t009Search.js +34 -0
- package/src/ressources/cmds.json +3 -0
- package/src/utils/shell/DatabaseShell.js +260 -0
- package/src/utils/shell/DatabaseShellBuilder.js +170 -0
- package/src/utils/shell/Shell.js +82 -0
- package/src/utils/shell/ShellBuilder.js +177 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import showChangelog from './modules/changelog.js';
|
|
|
17
17
|
import commands from "./ressources/cmds.json" with {type: 'json'};
|
|
18
18
|
import packageJson from "../package.json" with {type: 'json'}
|
|
19
19
|
import extdSearch from './modules/extdSearch.js';
|
|
20
|
+
import t009Search from './modules/t009Search.js';
|
|
20
21
|
import syncConfig from './modules/syncConfig.js';
|
|
21
22
|
import configMutation from './modules/configMutation.js';
|
|
22
23
|
import bundleProduct from './modules/bundleProduct.js';
|
|
@@ -89,6 +90,9 @@ switch (cli.input[0]) {
|
|
|
89
90
|
case "extd_search":
|
|
90
91
|
extdSearch();
|
|
91
92
|
break;
|
|
93
|
+
case "t009_search":
|
|
94
|
+
t009Search();
|
|
95
|
+
break;
|
|
92
96
|
case "sync_config":
|
|
93
97
|
syncConfig();
|
|
94
98
|
break;
|
|
@@ -1,200 +1,85 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import prompts from "prompts";
|
|
3
|
-
import CliTable3 from "cli-table3";
|
|
4
|
-
import readline from "readline";
|
|
5
|
-
import chalk from "chalk";
|
|
6
|
-
import fuzzysort from "fuzzysort";
|
|
1
|
+
import { DatabaseShellBuilder, TableConfig } from "../utils/shell/DatabaseShellBuilder.js";
|
|
7
2
|
|
|
8
3
|
// Entry point
|
|
9
4
|
export default async function extdSearch() {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
rl.on('line', async (input) => {
|
|
24
|
-
const query = input.trim();
|
|
25
|
-
|
|
26
|
-
if (handleBuiltInCommands(query, rl)) return;
|
|
27
|
-
|
|
28
|
-
const matchedConfig = configs.find(c => query === `:${c.cmd}`);
|
|
29
|
-
if (matchedConfig) {
|
|
30
|
-
currentSearchType = matchedConfig;
|
|
31
|
-
} else {
|
|
32
|
-
lastQuery = query;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const results = await fuzzySearch(baseData, lastQuery, currentSearchType);
|
|
36
|
-
renderTable(results, currentSearchType);
|
|
37
|
-
rl.prompt();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
rl.on('close', () => {
|
|
41
|
-
console.log(chalk.yellow("Search session ended."));
|
|
42
|
-
process.exit(0);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function createReadline() {
|
|
47
|
-
return readline.createInterface({
|
|
48
|
-
input: process.stdin,
|
|
49
|
-
output: process.stdout,
|
|
50
|
-
prompt: 'Search query (show help with :?)> '
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function handleBuiltInCommands(query, rl) {
|
|
55
|
-
switch (query) {
|
|
56
|
-
case ':q':
|
|
57
|
-
rl.close();
|
|
58
|
-
return true;
|
|
59
|
-
case ':?':
|
|
60
|
-
printHelp();
|
|
61
|
-
rl.prompt();
|
|
62
|
-
return true;
|
|
63
|
-
default:
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function printHelp() {
|
|
69
|
-
console.log("\n" + chalk.cyan("Available Commands:"));
|
|
70
|
-
console.log(chalk.yellow(":q") + " - Exit the application");
|
|
71
|
-
console.log(chalk.yellow(":?") + " - Show this help");
|
|
72
|
-
configs.forEach(config =>
|
|
73
|
-
console.log(`${chalk.yellow(`:${config.cmd}`)} - Switch to ${config.name}`)
|
|
74
|
-
);
|
|
75
|
-
console.log();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function configureDB() {
|
|
79
|
-
console.log();
|
|
80
|
-
const databaseNames = await DB.getDatabaseNames();
|
|
81
|
-
|
|
82
|
-
return await prompts([
|
|
83
|
-
{
|
|
84
|
-
type: 'autocomplete',
|
|
85
|
-
name: 'dbName',
|
|
86
|
-
message: 'DB-Name?',
|
|
87
|
-
choices: databaseNames.map(db => ({ title: db.name }))
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
type: 'select',
|
|
91
|
-
name: 'tables',
|
|
92
|
-
message: 'Search-Type?',
|
|
93
|
-
choices: [
|
|
94
|
-
{ title: 'EXTD/EXTI', value: 'EXTD/EXTI' },
|
|
95
|
-
{ title: 'EXTI only', value: 'EXTI' },
|
|
96
|
-
{ title: 'EXTD only', value: 'EXTD' }
|
|
97
|
-
]
|
|
98
|
-
}
|
|
99
|
-
], {
|
|
100
|
-
onCancel: () => {
|
|
101
|
-
console.log("\n" + chalk.red("Cancelled Search!\n"));
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function loadData(db, tableChoice) {
|
|
107
|
-
const extd = await getTableData('extd', db);
|
|
108
|
-
const exti = await getTableData('exti', db);
|
|
109
|
-
|
|
110
|
-
const cleaned = [
|
|
111
|
-
...cleanData(extd, 'extd'),
|
|
112
|
-
...cleanData(exti, 'exti')
|
|
113
|
-
];
|
|
114
|
-
|
|
115
|
-
if (tableChoice === 'EXTD') return cleaned.filter(e => e.table === 'extd');
|
|
116
|
-
if (tableChoice === 'EXTI') return cleaned.filter(e => e.table === 'exti');
|
|
117
|
-
return cleaned;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function cleanData(entries, tableName) {
|
|
121
|
-
return entries.map(entry => {
|
|
122
|
-
const stripped = Object.fromEntries(Object.entries(entry).map(([k, v]) => {
|
|
123
|
-
return [k.replace(/^ext[di]_/, ''), v];
|
|
124
|
-
}));
|
|
125
|
-
return { ...stripped, table: tableName };
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function getTableData(table, db) {
|
|
130
|
-
const query = `SELECT * FROM ${table};`;
|
|
131
|
-
return await DB.executeQueryOnDB(query, db);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function fuzzySearch(data, query, config) {
|
|
135
|
-
if (!query) return data.slice(0, 1000);
|
|
136
|
-
return fuzzysort.go(query, data, {
|
|
137
|
-
keys: config.searchKeys,
|
|
138
|
-
limit: 1000,
|
|
139
|
-
threshold: config.threshold
|
|
140
|
-
}).map(r => ({ ...r.obj, score: r.score }));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function renderTable(data, config) {
|
|
144
|
-
const table = new CliTable3({ head: config.tableHeader });
|
|
145
|
-
data.forEach(row => table.push(config.tableFormatter(row)));
|
|
146
|
-
console.log(table.toString());
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ---------- Configurations & Helpers ----------
|
|
150
|
-
|
|
151
|
-
const configs = [
|
|
152
|
-
{
|
|
153
|
-
name: "Overview",
|
|
154
|
-
cmd: "ow",
|
|
155
|
-
searchKeys: ['mnr', 'name', 'bez_d', 'bez_f', 'bez_i'],
|
|
156
|
-
threshold: 0.7,
|
|
157
|
-
tableHeader: ['MNR', 'Table', 'Name', 'Bezeichnung Deutsch', 'Bezeichnung Französisch', 'Bezeichnung Italieniesch'],
|
|
158
|
-
tableFormatter: p => [p.mnr, p.table, p.name, p.bez_d, p.bez_f, p.bez_i]
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: "Field-Information",
|
|
162
|
-
cmd: "fi",
|
|
163
|
-
searchKeys: [
|
|
5
|
+
const overviewConfig = new TableConfig()
|
|
6
|
+
.withName("Overview")
|
|
7
|
+
.withShortcut(":ow")
|
|
8
|
+
.withSearchKeys(["mnr", "name", "bez_d", "bez_f", "bez_i"])
|
|
9
|
+
.withThreshold(0.7)
|
|
10
|
+
.withHeader(["MNR", "Table", "Name", "Bezeichnung Deutsch", "Bezeichnung Französisch", "Bezeichnung Italienisch"])
|
|
11
|
+
.withFormatter(p => [p.mnr, p.table, p.name, p.bez_d, p.bez_f, p.bez_i]);
|
|
12
|
+
|
|
13
|
+
const fieldInfoConfig = new TableConfig()
|
|
14
|
+
.withName("Field-Information")
|
|
15
|
+
.withShortcut(":fi")
|
|
16
|
+
.withSearchKeys([
|
|
164
17
|
'name', 'bez_d', 'bez_f', 'bez_i',
|
|
165
18
|
'b_dtext_1', 'b_dtext_2', 'b_dtext_3',
|
|
166
19
|
'b_dtext_4', 'b_dtext_5', 'b_dtext_6',
|
|
167
20
|
'b_dtext_7', 'b_dtext_8', 'b_dtext_9'
|
|
168
|
-
]
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
21
|
+
])
|
|
22
|
+
.withThreshold(0.7)
|
|
23
|
+
.withHeader(['MNR', 'Table', 'Name', 'Feldtyp', 'Flag', 'Testflag', 'Wert 1', 'Wert 2', 'Wert 3', 'Wert 4', 'Wert 5', 'Wert 6', 'Wert 7', 'Wert 8', 'Wert 9'])
|
|
24
|
+
.withFormatter(p => [
|
|
172
25
|
p.mnr, p.table, p.name,
|
|
173
26
|
formatFieldType(p.special),
|
|
174
27
|
formatFlag(p.flag),
|
|
175
|
-
formatTestFlag(p.
|
|
28
|
+
formatTestFlag(p.testflab),
|
|
176
29
|
...Array.from({ length: 9 }, (_, i) => formatWert(p[`b_value_${i + 1}`], p[`b_dtext_${i + 1}`]))
|
|
177
|
-
]
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const dispFieldsConfig = new TableConfig()
|
|
33
|
+
.withName("Disp-Fields")
|
|
34
|
+
.withShortcut(":df")
|
|
35
|
+
.withSearchKeys([
|
|
183
36
|
'mnr', 'name', 'bez_d', 'testfeld',
|
|
184
37
|
'disp_feld_1', 'disp_feld_2', 'disp_feld_3',
|
|
185
38
|
'disp_feld_4', 'disp_feld_5', 'disp_feld_6',
|
|
186
39
|
'disp_feld_7', 'disp_feld_8', 'disp_feld_9'
|
|
187
|
-
]
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
40
|
+
])
|
|
41
|
+
.withThreshold(0.7)
|
|
42
|
+
.withHeader(['MNR', 'Table', 'Name', 'Test-Feld', 'Dispmask', 'Dispfeld 1', 'Dispfeld 2', 'Dispfeld 3', 'Dispfeld 4', 'Dispfeld 5', 'Dispfeld 6', 'Dispfeld 7', 'Dispfeld 8', 'Dispfeld 9'])
|
|
43
|
+
.withFormatter(p => [
|
|
191
44
|
p.mnr, p.table, p.name, p.testfeld, p.dispmask,
|
|
192
45
|
p.disp_feld_1, p.disp_feld_2, p.disp_feld_3,
|
|
193
46
|
p.disp_feld_4, p.disp_feld_5, p.disp_feld_6,
|
|
194
47
|
p.disp_feld_7, p.disp_feld_8, p.disp_feld_9
|
|
195
|
-
]
|
|
196
|
-
|
|
197
|
-
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const builder = new DatabaseShellBuilder();
|
|
51
|
+
builder
|
|
52
|
+
.withEditTools(
|
|
53
|
+
[
|
|
54
|
+
{
|
|
55
|
+
title: 'MNR', column: 'mnr', default: '1'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: 'Name', column: 'name', default: null,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: 'Bezeichnung Deutsch', column: 'bez_d', default: null,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: 'Bezeichnung Französisch', column: 'bez_f', default: null,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: 'Bezeichnung Italienisch', column: 'bez_i', default: null
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
["name"],
|
|
71
|
+
["name"],
|
|
72
|
+
["mnr", "bez_d", "bez_f", "bez_i"])
|
|
73
|
+
.withConfig(overviewConfig)
|
|
74
|
+
.withConfig(fieldInfoConfig)
|
|
75
|
+
.withConfig(dispFieldsConfig)
|
|
76
|
+
.withTables("EXTD/EXTI", ["extd", "exti"])
|
|
77
|
+
.withTables("EXTD", ["extd"])
|
|
78
|
+
.withTables("EXTI", ["exti"]);
|
|
79
|
+
|
|
80
|
+
const shell = builder.build();
|
|
81
|
+
await shell.run();
|
|
82
|
+
}
|
|
198
83
|
|
|
199
84
|
function formatWert(value, bez) {
|
|
200
85
|
return value || bez ? `'${value}'='${bez}'` : '';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DatabaseShellBuilder, TableConfig } from "../utils/shell/DatabaseShellBuilder.js";
|
|
2
|
+
|
|
3
|
+
export default async function t009Search() {
|
|
4
|
+
|
|
5
|
+
const overviewConfig = new TableConfig()
|
|
6
|
+
.withName("Overview")
|
|
7
|
+
.withShortcut(":ow")
|
|
8
|
+
.withSearchKeys(["mnr", "grp_pw", "lnr", "pgm", "flag", "bez", "bez_f", "bez_i"])
|
|
9
|
+
.withThreshold(0.7)
|
|
10
|
+
.withHeader(["MNR", "Mitarbeitergruppe", "Zeilennummer", "Programm-Name", "Typ (P=Programm, S=Shell)", "Bezeichnung", "Bezeichnung franz.", "Bezeichnung ital."])
|
|
11
|
+
.withFormatter(p => [p.mnr, p.grp_pw, p.lnr, p.pgm, p.flag, p.bez, p.bez_f, p.bez_i]);
|
|
12
|
+
|
|
13
|
+
const builder = new DatabaseShellBuilder();
|
|
14
|
+
builder
|
|
15
|
+
.withEditTools(
|
|
16
|
+
[
|
|
17
|
+
{ title: 'MNR', column: 'mnr', default: '1' },
|
|
18
|
+
{ title: 'Mitarbeitergruppe', column: 'grp_pw', default: '' },
|
|
19
|
+
{ title: 'Zeilennummer', column: 'lnr', default: null },
|
|
20
|
+
{ title: 'Programm-Name', column: 'pgm', default: null },
|
|
21
|
+
{ title: 'Typ', column: 'flag', default: 'P' },
|
|
22
|
+
{ title: 'Bezeichnung', column: 'bez', default: null },
|
|
23
|
+
{ title: 'Bezeichnung franz.', column: 'bez_f', default: null },
|
|
24
|
+
{ title: 'Bezeichnung ital.', column: 'bez_i', default: null },
|
|
25
|
+
],
|
|
26
|
+
["mnr", "pgm"],
|
|
27
|
+
["mnr", "lnr", "pgm", "bez"],
|
|
28
|
+
["grp_pw", "flag", "bez_f", "bez_i"])
|
|
29
|
+
.withConfig(overviewConfig)
|
|
30
|
+
.withTables("T009", ["t009"]);
|
|
31
|
+
|
|
32
|
+
const shell = builder.build();
|
|
33
|
+
await shell.run();
|
|
34
|
+
}
|
package/src/ressources/cmds.json
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Shell } from "./shell.js";
|
|
3
|
+
import { DB } from "../db/DB.js";
|
|
4
|
+
import fuzzysort from "fuzzysort";
|
|
5
|
+
import CliTable3 from "cli-table3";
|
|
6
|
+
import prompts from "prompts";
|
|
7
|
+
import { TableConfig } from "./DatabaseShellBuilder.js";
|
|
8
|
+
|
|
9
|
+
export class DatabaseShell {
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
/**
|
|
13
|
+
* The currently active tables to load the data from
|
|
14
|
+
* @type {string[]}
|
|
15
|
+
* @private
|
|
16
|
+
*/
|
|
17
|
+
this.currentTables = [];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The currently active config
|
|
21
|
+
* @type {TableConfig}
|
|
22
|
+
*/
|
|
23
|
+
this.currentConfig = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The active database
|
|
27
|
+
* @type {string}
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
this.db = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The cached data of the active table
|
|
34
|
+
* @type {object[]}
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
this.data = [];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The tables the user can select to use
|
|
41
|
+
* @type {object[]}
|
|
42
|
+
*/
|
|
43
|
+
this.tables = [];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The underlying shell to handle user input
|
|
47
|
+
* @type {Shell}
|
|
48
|
+
*/
|
|
49
|
+
this.shell = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Configures the database shell and runs the underlying shell
|
|
54
|
+
*/
|
|
55
|
+
async run() {
|
|
56
|
+
await this.configure();
|
|
57
|
+
await this.shell.run();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Searches for a string based on the current shell state and prints the results to the console.
|
|
62
|
+
* @param {string} query The search string
|
|
63
|
+
*/
|
|
64
|
+
executeSearch(query) {
|
|
65
|
+
const results = this.fuzzySearch(query);
|
|
66
|
+
this.renderTable(results);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async deleteRow(row) {
|
|
70
|
+
const table = row.table;
|
|
71
|
+
const where = Object.entries(row).filter(([k, v]) => k !== "table").map(([k, v]) => `\`${table}_${k}\`='${String(v).replaceAll("'", "\\'")}'`).join(' AND ');
|
|
72
|
+
const sql = `DELETE FROM \`${table}\` WHERE ${where}`;
|
|
73
|
+
await DB.executeQueryOnDB(sql, this.db);
|
|
74
|
+
await this.loadData();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async insertRow(row) {
|
|
78
|
+
const table = row.table;
|
|
79
|
+
const kvPairs = Object.entries(row).filter(([k, v]) => k !== "table");
|
|
80
|
+
const sql = `INSERT INTO \`${table}\` (${kvPairs.map(([k, v]) => `\`${table}_${k}\``).join(', ')}) VALUES (${kvPairs.map(([k, v]) => `'${String(v).replaceAll("'", "\\'")}'`)});`;
|
|
81
|
+
await DB.executeQueryOnDB(sql, this.db);
|
|
82
|
+
await this.loadData();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async updateRow(row, whereRow) {
|
|
86
|
+
const table = whereRow.table;
|
|
87
|
+
const set = Object.entries(row).filter(([k, v]) => k !== "table").map(([k, v]) => `\`${table}_${k}\`='${String(v).replaceAll("'", "\\'")}'`).join(', ');
|
|
88
|
+
const where = Object.entries(whereRow).filter(([k, v]) => k !== "table").map(([k, v]) => `\`${table}_${k}\`='${String(v).replaceAll("'", "\\'")}'`).join(' AND ');
|
|
89
|
+
const sql = `UPDATE \`${table}\` SET ${set} WHERE ${where};`;
|
|
90
|
+
await DB.executeQueryOnDB(sql, this.db);
|
|
91
|
+
await this.loadData();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async editRow(data, columns, requiredFields, optionalFields) {
|
|
95
|
+
let cancelled = false;
|
|
96
|
+
const questions = [...requiredFields, ...optionalFields]
|
|
97
|
+
.filter(f => Object.hasOwn(data, f))
|
|
98
|
+
.map(f => ({
|
|
99
|
+
type: 'text',
|
|
100
|
+
message: columns.find(c => c.column === f).title,
|
|
101
|
+
name: f,
|
|
102
|
+
initial: columns.find(c => c.column === f).default ?? data[f]}));
|
|
103
|
+
|
|
104
|
+
const responses = await prompts(questions, { onCancel: () => { cancelled = true; return false; } });
|
|
105
|
+
|
|
106
|
+
if(cancelled) return null;
|
|
107
|
+
|
|
108
|
+
const newData = {
|
|
109
|
+
...data,
|
|
110
|
+
...responses
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
return newData;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async selectRow(columns, requiredColumns) {
|
|
118
|
+
let cancelled = false;
|
|
119
|
+
const questions = requiredColumns.map(f => ({
|
|
120
|
+
type: 'text',
|
|
121
|
+
message: columns.find(c => c.column === f).title,
|
|
122
|
+
name: f,
|
|
123
|
+
initial: columns.find(c => c.column === f).default
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
const responses = await prompts(questions, { onCancel: () => { cancelled = true; return false; } });
|
|
127
|
+
if(cancelled) return null;
|
|
128
|
+
const results = this.data.filter(d => Object.entries(responses).every(([k, v]) => d[k] == v));
|
|
129
|
+
|
|
130
|
+
if(results.length == 0) {
|
|
131
|
+
console.log(chalk.red("No results found."));
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if(results.length > 1) {
|
|
136
|
+
console.log(chalk.red("Multiple results found. Please select one from the list..."));
|
|
137
|
+
const selection = await prompts({
|
|
138
|
+
type: 'autocomplete',
|
|
139
|
+
choices: results.map(r => ({
|
|
140
|
+
title: Object.entries(r)
|
|
141
|
+
.filter(([k, v]) => k === "table" || columns.some(c => c.column === k))
|
|
142
|
+
.map(([k, v]) => `${k === "table" ? "Table" : columns.find(c => c.column === k).title}: ${v}`)
|
|
143
|
+
.join(", "),
|
|
144
|
+
value: r
|
|
145
|
+
})),
|
|
146
|
+
message: "Select Row",
|
|
147
|
+
name: 'result'
|
|
148
|
+
}, { onCancel: () => { cancelled = true; return false; } });
|
|
149
|
+
|
|
150
|
+
if(cancelled) return null;
|
|
151
|
+
|
|
152
|
+
return selection.result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return results[0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Loads the rows from the currently active tables into the shell
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
async loadData() {
|
|
163
|
+
const data = [];
|
|
164
|
+
|
|
165
|
+
for(let i = 0; i < this.currentTables.length; i++) {
|
|
166
|
+
const tableData = await this.getTableData(this.currentTables[i]);
|
|
167
|
+
data.push(...this.cleanData(tableData, this.currentTables[i]));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.data = data;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Loads all rows from a table of the currently active database
|
|
175
|
+
* @param {string} table The table name
|
|
176
|
+
* @returns {Promise<any>}
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
async getTableData(table) {
|
|
180
|
+
const query = `SELECT * FROM ${table};`;
|
|
181
|
+
return await DB.executeQueryOnDB(query, this.db);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Removes the table name prefix from the individual rows
|
|
186
|
+
* @param {object[]} entries The rows of the table
|
|
187
|
+
* @param {string} tableName The name of the table where the data is from
|
|
188
|
+
* @returns {object[]}
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
191
|
+
cleanData(entries, tableName) {
|
|
192
|
+
return entries.map(entry => {
|
|
193
|
+
const stripped = Object.fromEntries(
|
|
194
|
+
Object.entries(entry).map(([k, v]) => [
|
|
195
|
+
k.replace(new RegExp(`^${tableName}_`), ''),
|
|
196
|
+
v
|
|
197
|
+
])
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return { ...stripped, table: tableName };
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Executes a query on the currently loaded table using fuzzysearch
|
|
206
|
+
* @param {string} query The search query
|
|
207
|
+
* @returns {object[]}
|
|
208
|
+
* @private
|
|
209
|
+
*/
|
|
210
|
+
fuzzySearch(query) {
|
|
211
|
+
if(!query) return this.data.slice(0, 1000);
|
|
212
|
+
|
|
213
|
+
return fuzzysort.go(query, this.data, {
|
|
214
|
+
keys: this.currentConfig.searchKeys,
|
|
215
|
+
limit: 1000,
|
|
216
|
+
threshold: this.currentConfig.threshold
|
|
217
|
+
}).map(r => ({ ...r.obj, score: r.score }));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Renders a table in the console using the currently active config
|
|
222
|
+
* @param {object[]} tableData The table data to render
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
renderTable(tableData) {
|
|
226
|
+
const table = new CliTable3({ head: this.currentConfig.tableHeader });
|
|
227
|
+
tableData.forEach(row => {
|
|
228
|
+
table.push(this.currentConfig.tableFormatter(row));
|
|
229
|
+
});
|
|
230
|
+
console.log(table.toString());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Prompts the user for the database and search type
|
|
235
|
+
*/
|
|
236
|
+
async configure() {
|
|
237
|
+
console.log();
|
|
238
|
+
const databaseNames = await DB.getDatabaseNames();
|
|
239
|
+
|
|
240
|
+
const results = await prompts([
|
|
241
|
+
{
|
|
242
|
+
type: 'autocomplete',
|
|
243
|
+
name: 'dbName',
|
|
244
|
+
message: 'DB-Name?',
|
|
245
|
+
choices: databaseNames.map(db => ({ title: db.name }))
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: 'select',
|
|
249
|
+
name: 'tables',
|
|
250
|
+
message: 'Search-Type?',
|
|
251
|
+
choices: this.tables.map(t => ({ title: t.name, value: t.tables }))
|
|
252
|
+
}
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
this.db = results.dbName;
|
|
256
|
+
this.currentTables = results.tables;
|
|
257
|
+
|
|
258
|
+
await this.loadData();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Shell } from "./shell.js";
|
|
3
|
+
import { ShellBuilder } from "./ShellBuilder.js"
|
|
4
|
+
import { DatabaseShell } from "./DatabaseShell.js";
|
|
5
|
+
|
|
6
|
+
export class DatabaseShellBuilder {
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
/**
|
|
10
|
+
* The available configs for the shell
|
|
11
|
+
* @type {TableConfig}
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
this.configs = [];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The available tables for the shell
|
|
18
|
+
* @type {object[]}
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
this.tables = [];
|
|
22
|
+
|
|
23
|
+
this.editTools = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Adds a table config to the shell
|
|
28
|
+
* @param {TableConfig} tableConfig The config to add
|
|
29
|
+
* @returns {DatabaseShellBuilder}
|
|
30
|
+
*/
|
|
31
|
+
withConfig(tableConfig) {
|
|
32
|
+
this.configs.push(tableConfig);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adds a source table to the shell
|
|
38
|
+
* @param {string} name The name displayed in the selection
|
|
39
|
+
* @param {string[]} tables The names of the tables where to load the data from
|
|
40
|
+
* @returns {DatabaseShellBuilder}
|
|
41
|
+
*/
|
|
42
|
+
withTables(name, tables) {
|
|
43
|
+
this.tables.push({ name: name, tables: tables });
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Enables edit tools with the specified selectors and values
|
|
49
|
+
* @param {{title: string, column: string, default: string}[]} columns The colums that are used in the editor
|
|
50
|
+
* @param {string[]} requiredSelectors The columns that are required to be filled in to select an entry to edit
|
|
51
|
+
* @param {string[]} requiredValues The columns that are required to be filled to update an entry
|
|
52
|
+
* @param {string[]} optionalValues The columns that can be optionally updated
|
|
53
|
+
* @returns {DatabaseShellBuilder}
|
|
54
|
+
*/
|
|
55
|
+
withEditTools(columns, requiredSelectors, requiredValues, optionalValues) {
|
|
56
|
+
this.editTools = {
|
|
57
|
+
columns: columns,
|
|
58
|
+
requiredSelectors: requiredSelectors,
|
|
59
|
+
requiredValues: requiredValues,
|
|
60
|
+
optionalValues: optionalValues
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds the Shell
|
|
68
|
+
* @returns {DatabaseShell}
|
|
69
|
+
*/
|
|
70
|
+
build() {
|
|
71
|
+
const databaseShell = new DatabaseShell();
|
|
72
|
+
|
|
73
|
+
const builder = new ShellBuilder();
|
|
74
|
+
builder.withBuiltInHandler()
|
|
75
|
+
.withBuiltInCommands()
|
|
76
|
+
.withCommandHandler((_, input) => { databaseShell.executeSearch(input); return true; })
|
|
77
|
+
.withCommand(":cfg", "Reconfigure the Database and Search Type", async () => await databaseShell.configure());
|
|
78
|
+
|
|
79
|
+
this.configs.forEach(config => {
|
|
80
|
+
builder.withCommand(config.cmdShortcut, `Switch to ${config.name}`, () => {
|
|
81
|
+
databaseShell.currentConfig = config;
|
|
82
|
+
console.log(`Switched to ${chalk.yellow(config.name)}.`);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if(this.editTools !== null) {
|
|
87
|
+
builder.withCommand(":del", "Delete a row", async (shell, input, args) => {
|
|
88
|
+
console.log(chalk.yellow("Select the row to delete: "));
|
|
89
|
+
const rowData = await databaseShell.selectRow(this.editTools.columns, this.editTools.requiredSelectors);
|
|
90
|
+
if(!rowData) return;
|
|
91
|
+
|
|
92
|
+
await databaseShell.deleteRow(rowData);
|
|
93
|
+
console.log(chalk.red("Row deleted"));
|
|
94
|
+
})
|
|
95
|
+
.withCommand(":dup", "Duplicate a row", async (shell, input, args) => {
|
|
96
|
+
console.log(chalk.yellow("Select the row to duplicate: "));
|
|
97
|
+
const rowData = await databaseShell.selectRow(this.editTools.columns, this.editTools.requiredSelectors);
|
|
98
|
+
if(!rowData) return;
|
|
99
|
+
|
|
100
|
+
console.log(chalk.yellow("Enter the updated row data: "));
|
|
101
|
+
const editedData = await databaseShell.editRow(rowData, this.editTools.columns, this.editTools.requiredValues, this.editTools.optionalValues);
|
|
102
|
+
if(!editedData) return;
|
|
103
|
+
|
|
104
|
+
await databaseShell.insertRow(editedData);
|
|
105
|
+
console.log(chalk.green("Row duplicated"));
|
|
106
|
+
})
|
|
107
|
+
.withCommand(":mod", "Modify an existing row", async (shell, input, args) => {
|
|
108
|
+
console.log(chalk.yellow("Select the row to edit: "));
|
|
109
|
+
const rowData = await databaseShell.selectRow(this.editTools.columns, this.editTools.requiredSelectors);
|
|
110
|
+
if(rowData === null) return;
|
|
111
|
+
|
|
112
|
+
console.log(chalk.yellow("Enter the updated row data: "));
|
|
113
|
+
const editedData = await databaseShell.editRow(rowData, this.editTools.columns, this.editTools.requiredValues, this.editTools.optionalValues);
|
|
114
|
+
if(!editedData) return;
|
|
115
|
+
|
|
116
|
+
await databaseShell.updateRow(editedData, rowData);
|
|
117
|
+
console.log(chalk.yellow("Row updated"));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const shell = builder.build();
|
|
122
|
+
|
|
123
|
+
databaseShell.shell = shell;
|
|
124
|
+
databaseShell.currentConfig = this.configs[0];
|
|
125
|
+
databaseShell.tables = this.tables;
|
|
126
|
+
|
|
127
|
+
return databaseShell;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class TableConfig {
|
|
132
|
+
constructor() {
|
|
133
|
+
this.name = "";
|
|
134
|
+
this.cmdShortcut = "";
|
|
135
|
+
this.searchKeys = [];
|
|
136
|
+
this.threshold = 0.7;
|
|
137
|
+
this.tableHeader = [];
|
|
138
|
+
this.tableFormatter = () => [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
withName(name) {
|
|
142
|
+
this.name = name;
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
withShortcut(shortcut) {
|
|
147
|
+
this.cmdShortcut = shortcut;
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
withSearchKeys(keys) {
|
|
152
|
+
this.searchKeys = keys;
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
withThreshold(threshold) {
|
|
157
|
+
this.threshold = threshold;
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
withHeader(tableHeader) {
|
|
162
|
+
this.tableHeader = tableHeader;
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
withFormatter(formatter) {
|
|
167
|
+
this.tableFormatter = formatter;
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import prompts from "prompts";
|
|
3
|
+
|
|
4
|
+
export class Shell {
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.commands = [];
|
|
8
|
+
this.commandHandlers = [];
|
|
9
|
+
this.name = "";
|
|
10
|
+
this.prompt = "";
|
|
11
|
+
this.autocomplete = [];
|
|
12
|
+
|
|
13
|
+
this.exitRequested = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Starts the shell in a loop and continues until exit() is called.
|
|
18
|
+
*/
|
|
19
|
+
async run() {
|
|
20
|
+
this.exitRequested = false;
|
|
21
|
+
|
|
22
|
+
console.log(chalk.green(`${this.name} Started.`));
|
|
23
|
+
|
|
24
|
+
while (!this.exitRequested) {
|
|
25
|
+
const response = await prompts({
|
|
26
|
+
type: "autocomplete",
|
|
27
|
+
name: "input",
|
|
28
|
+
message: this.prompt,
|
|
29
|
+
choices: this.autocomplete.map(ac => ({
|
|
30
|
+
title: ac,
|
|
31
|
+
value: ac
|
|
32
|
+
})),
|
|
33
|
+
limit: 1,
|
|
34
|
+
suggest: async (input, choices) => {
|
|
35
|
+
const text = input ?? "";
|
|
36
|
+
const q = text.trim().toLowerCase();
|
|
37
|
+
|
|
38
|
+
const matches = q.length === 0
|
|
39
|
+
? choices
|
|
40
|
+
: choices.filter(choice => choice.title.toLowerCase().startsWith(q));
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
title: text.length > 0 ? text : " ",
|
|
45
|
+
value: text
|
|
46
|
+
},
|
|
47
|
+
...matches.filter(choice => choice.value !== text)
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const input = (response?.input ?? "").trim();
|
|
53
|
+
|
|
54
|
+
if (!await this.handleCommand(input)) {
|
|
55
|
+
console.log(chalk.red(`Unknown command '${input}'`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(chalk.yellow(`\n\n${this.name} Ended.`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Requests the shell to exit
|
|
64
|
+
*/
|
|
65
|
+
exit() {
|
|
66
|
+
this.exitRequested = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Executes a single command in the shell
|
|
71
|
+
* @param {String} input The command to execute
|
|
72
|
+
* @returns {Promise<boolean>}
|
|
73
|
+
*/
|
|
74
|
+
async handleCommand(input) {
|
|
75
|
+
for(let i = 0; i < this.commandHandlers.length; i++) {
|
|
76
|
+
const handler = this.commandHandlers[i];
|
|
77
|
+
if(await handler(this, input))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Shell } from "./shell.js";
|
|
3
|
+
|
|
4
|
+
export class ShellBuilder {
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.commands = [];
|
|
8
|
+
this.commandHandlers = [];
|
|
9
|
+
this.name = "Interactive Shell";
|
|
10
|
+
this.prompt = "Interactive Shell (show help with :?)";
|
|
11
|
+
this.useBuiltInHandler = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Adds the built-in command handler
|
|
16
|
+
* @returns {ShellBuilder}
|
|
17
|
+
*/
|
|
18
|
+
withBuiltInHandler() {
|
|
19
|
+
this.withCommandHandler(async (shell, input) => {
|
|
20
|
+
const parts = this.parseArgs(input);
|
|
21
|
+
if(parts.length == 0) return false;
|
|
22
|
+
|
|
23
|
+
const commandName = parts[0];
|
|
24
|
+
const args = parts.slice(1);
|
|
25
|
+
|
|
26
|
+
for(let i = 0; i < shell.commands.length; i++) {
|
|
27
|
+
const command = shell.commands[i];
|
|
28
|
+
|
|
29
|
+
if(command.name == commandName) {
|
|
30
|
+
await command.handler(shell, input, ...args);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.useBuiltInHandler = true;
|
|
39
|
+
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adds the built-in commands (:q and :?), requires a supported handler to work
|
|
45
|
+
* @returns {ShellBuilder}
|
|
46
|
+
*/
|
|
47
|
+
withBuiltInCommands() {
|
|
48
|
+
this.withCommand(":q", "Exit the shell", shell => {
|
|
49
|
+
shell.exit();
|
|
50
|
+
})
|
|
51
|
+
.withCommand(":?", "Shows this help", shell => {
|
|
52
|
+
console.log("\n" + chalk.cyan("Available Commands:"));
|
|
53
|
+
|
|
54
|
+
shell.commands.forEach(command => {
|
|
55
|
+
console.log(`${chalk.yellow(command.name)} - ${command.helpText}`);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add a command to be executed by the built in command handler
|
|
64
|
+
* @param {String} commandName The command name to be entered in the command line
|
|
65
|
+
* @param {String} helpText The text displayed in the help
|
|
66
|
+
* @param {(shell: Shell, input: String, ...args: String[]) => void} handler The handler to be executed with the executing shell passed as an arugment
|
|
67
|
+
* @returns {ShellBuilder}
|
|
68
|
+
*/
|
|
69
|
+
withCommand(commandName, helpText, handler) {
|
|
70
|
+
this.commands.push({
|
|
71
|
+
name: commandName,
|
|
72
|
+
handler: handler,
|
|
73
|
+
helpText: helpText
|
|
74
|
+
});
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Change the name of the shell
|
|
80
|
+
* @param {String} name The name of the shell used in some texts
|
|
81
|
+
* @returns {ShellBuilder}
|
|
82
|
+
*/
|
|
83
|
+
withName(name) {
|
|
84
|
+
this.name = name;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Change the prompt displayed when entering text
|
|
90
|
+
* @param {String} prompt The text to be displayed in the prompt
|
|
91
|
+
* @returns {ShellBuilder}
|
|
92
|
+
*/
|
|
93
|
+
withPrompt(prompt) {
|
|
94
|
+
this.prompt = prompt;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Add a command handler to be executed if the inputted command doesn't correspond to a built in handler
|
|
100
|
+
* @param {(shell: Shell, input: String) => boolean} handler The handler to be executed with the inputted text and shell passed as arguments.
|
|
101
|
+
* Return true to signal that the handler was successful and shouldn't continue with another handler.
|
|
102
|
+
*
|
|
103
|
+
* @returns {ShellBuilder}
|
|
104
|
+
*/
|
|
105
|
+
withCommandHandler(handler) {
|
|
106
|
+
this.commandHandlers.push(handler);
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates a usable shell with the built-in and custom functions.
|
|
112
|
+
* @returns {Shell}
|
|
113
|
+
*/
|
|
114
|
+
build() {
|
|
115
|
+
let shell = new Shell();
|
|
116
|
+
shell.commands = this.commands;
|
|
117
|
+
shell.name = this.name;
|
|
118
|
+
shell.prompt = this.prompt;
|
|
119
|
+
shell.commandHandlers = this.commandHandlers;
|
|
120
|
+
shell.autocomplete = shell.commands.map(c => c.name);
|
|
121
|
+
|
|
122
|
+
return shell;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Splits a command into args with support for spaces and and escape sequences
|
|
127
|
+
* @private
|
|
128
|
+
* @param {String} input The unformatted command
|
|
129
|
+
* @returns {String[]}
|
|
130
|
+
*/
|
|
131
|
+
parseArgs(input) {
|
|
132
|
+
const result = [];
|
|
133
|
+
let current = "";
|
|
134
|
+
let inQuotes = false;
|
|
135
|
+
let escaping = false;
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < input.length; i++) {
|
|
138
|
+
const char = input[i];
|
|
139
|
+
|
|
140
|
+
if (escaping) {
|
|
141
|
+
current += char;
|
|
142
|
+
escaping = false;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (char === "\\") {
|
|
147
|
+
escaping = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (char === '"') {
|
|
152
|
+
inQuotes = !inQuotes;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (char === " " && !inQuotes) {
|
|
157
|
+
if (current.length > 0) {
|
|
158
|
+
result.push(current);
|
|
159
|
+
current = "";
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
current += char;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (escaping) {
|
|
168
|
+
current += "\\";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (current.length > 0) {
|
|
172
|
+
result.push(current);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
}
|