diff --git a/package-lock.json b/package-lock.json index 98e5d0e..1d4d3ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1938,6 +1938,15 @@ "request": "^2.86.0" } }, + "cron-parser": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.13.0.tgz", + "integrity": "sha512-UWeIpnRb0eyoWPVk+pD3TDpNx3KCFQeezO224oJIkktBrcW6RoAPOx5zIKprZGfk6vcYSmA8yQXItejSaDBhbQ==", + "requires": { + "is-nan": "^1.2.1", + "moment-timezone": "^0.5.25" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -2091,7 +2100,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -4469,6 +4477,14 @@ "is-extglob": "^2.1.1" } }, + "is-nan": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.2.1.tgz", + "integrity": "sha1-n69ltvttskt/XAYoR16nH5iEAeI=", + "requires": { + "define-properties": "^1.1.1" + } + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -5979,6 +5995,19 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moment-timezone": { + "version": "0.5.27", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz", + "integrity": "sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==", + "requires": { + "moment": ">= 2.9.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6179,8 +6208,7 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", diff --git a/package.json b/package.json index 83e1421..de7196c 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/shelljs": "^0.8.5", "@types/table": "^4.0.7", "chalk": "^2.4.2", + "cron-parser": "^2.13.0", "flexi-path": "^0.1.2-beta.16", "nordvpn-server-lister": "https://github.com/jaspenlind/nordvpn-server-lister/releases/download/v0.0.1-alpha.3/nordvpn-server-lister-0.0.1-alpha.3.tgz", "option-parser": "https://github.com/jaspenlind/option-parser/releases/download/v0.0.1-alpha.6/option-parser-0.0.1-alpha.6.tgz", diff --git a/src/lib/arrayHelper.ts b/src/lib/arrayHelper.ts index f5fcba8..c695047 100644 --- a/src/lib/arrayHelper.ts +++ b/src/lib/arrayHelper.ts @@ -1,8 +1,67 @@ -export const any = (array: ArrayLike) => array.length > 0; +type NullableArray = ArrayLike | null; -export const isEmpty = (array: ArrayLike) => !any(array); +export const indeces = { + empty: 0, + first: 0, + lastIndexSubtrahend: 1 +}; -export const last = (array: ArrayLike): T => array[array.length - 1]; +export const any = (array: NullableArray) => + (array && array.length > indeces.empty) || false; + +export const isEmpty = (array: NullableArray) => !any(array); + +export const firstOrDefault = ( + array: NullableArray, + defaultValue: T | null = null +): T | null => (array && array[indeces.first]) || defaultValue; + +export const first = (array: NullableArray) => + firstOrDefault(array) || undefined; + +export const lastOrDefault = ( + array: NullableArray, + defaultValue: T | null = null +): T | null => + (array && array[array.length - indeces.lastIndexSubtrahend]) || defaultValue; + +export const last = (array: NullableArray) => + lastOrDefault(array) || undefined; + +export const takeWhile = ( + array: T[], + predicate: (item: T) => boolean +): T[] => { + const items: T[] = []; + + for (const item of array) { + if (!predicate(item)) { + break; + } + + items.push(item); + } + + return items; +}; + +export const takeWhileAll = ( + array: T[], + predicate: (current: T[]) => boolean +): T[] => { + const result: T[] = []; + + for (let index = 0; index < array.length; index += 1) { + result.push(array[index]); + + if (!predicate(result)) { + result.pop(); + break; + } + } + + return result; +}; export const distinct = (...items: T[]): T[] => items.filter((value: T, index: number, self: T[]) => { diff --git a/src/lib/cli.ts b/src/lib/cli.ts index 2c96373..368f8c0 100755 --- a/src/lib/cli.ts +++ b/src/lib/cli.ts @@ -40,7 +40,7 @@ const runCommand = (command: CommandDeclaration, options: OptionMap) => { const message = [`Excuting: router ${chalk.bold(command.fullName)}`]; if (any(command.args)) { - message.push(`with options ${command.args.join(" ")}`); + message.push(` with options '${command.args.join(" ")}'`); } message.push(" ..."); diff --git a/src/lib/commands/jobs/list.ts b/src/lib/commands/jobs/list.ts index f7a2b1b..4ea5a5c 100644 --- a/src/lib/commands/jobs/list.ts +++ b/src/lib/commands/jobs/list.ts @@ -1,10 +1,43 @@ +import cron from "cron-parser"; +import { table } from "table"; +import { isEmpty } from "../../arrayHelper"; +import { ScheduledItem } from "../../../types"; +import { parse } from "../../scheduledItemParser"; import ssh from "../../ssh"; import { create } from "../../../models/command"; const description = "Lists existing cron jobs"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const asTabular = (data: ScheduledItem[]): any[] => { + if (isEmpty(data)) return []; + + const tableValues = data.map(x => { + const values = Object.values(x); + const expression = cron.parseExpression(x.cronExpression); + values.push(expression.hasNext() ? expression.next() : ""); + return values; + }); + + const headings = Object.getOwnPropertyNames(data[0]); + headings.push("next run"); + + tableValues.unshift(headings); + + return tableValues; +}; + const run = () => { - ssh.execute("cru l"); + const result = ssh.execute("cru l", { silent: true }); + + const parsed = result.stdout + .split("\n") + .map(x => parse(x)) + .filter(x => x.id); + + const tabularData = asTabular(parsed); + console.log(parsed); + console.log(table(tabularData)); }; export default create({ description, run }); diff --git a/src/lib/cronParser.ts b/src/lib/cronParser.ts deleted file mode 100644 index 0b977d2..0000000 --- a/src/lib/cronParser.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { StringComparison } from "../types"; -import { create, empty, ParseResult } from "../types/ParseResult"; -import { parse } from "./enumHelper"; -import { swap } from "./mapHelper"; - -export enum CronSchedule { - Custom, - Daily, - Hourly, - Monthly, - Weekly, - Yearly -} - -const cronScheduleToTab = new Map([ - [CronSchedule.Daily, "0 0 * * *"], - [CronSchedule.Hourly, "0 * * * *"], - [CronSchedule.Monthly, "0 0 1 * *"], - [CronSchedule.Weekly, "0 0 * * 0"], - [CronSchedule.Yearly, "0 0 1 1 *"] -]); - -const cronTabToSchedule = swap(cronScheduleToTab); - -export const parseSchedule = (cronTab: string): ParseResult => - create(cronTabToSchedule.get(cronTab)); - -export const parseCronTab = ( - schedule: CronSchedule | string -): ParseResult => { - const key = - typeof schedule === "string" - ? parse(CronSchedule, schedule, StringComparison.OrdinalIgnoreCase).value - : schedule; - - const value = (key && cronScheduleToTab.get(key)) || undefined; - - return create(value); -}; diff --git a/src/lib/scheduledItemParser.ts b/src/lib/scheduledItemParser.ts new file mode 100644 index 0000000..f12a843 --- /dev/null +++ b/src/lib/scheduledItemParser.ts @@ -0,0 +1,39 @@ +import { ScheduledItem } from "../types"; +import { takeWhile, takeWhileAll } from "./arrayHelper"; + +const idMarker = "#"; + +const parseCronExpression = (row: string[]): [string, string[]] => { + const result = takeWhileAll(row, x => x.filter(z => z === " ").length < 5); + const remainder = row.slice(result.length + 1); + + return [result.join("").trim(), remainder]; +}; + +const parseCommand = (row: string[]): [string, string[]] => { + const result = takeWhile(row, x => x !== idMarker); + const remainder = row.slice(result.length + 1); + + return [result.join("").trim(), remainder]; +}; + +const parseId = (row: string[]): string => { + return row + .join("") + .replace(new RegExp(`${idMarker}`, "g"), "") + .trim(); +}; + +export const parse = (row: string): ScheduledItem => { + const [cronExpression, cronRemainder] = parseCronExpression([...row]); + const [command, commandRemainder] = parseCommand(cronRemainder); + const id = parseId(commandRemainder); + + const item: ScheduledItem = { + id, + command, + cronExpression + }; + + return item; +}; diff --git a/src/models/scheduledItem.ts b/src/models/scheduledItem.ts new file mode 100644 index 0000000..abd659e --- /dev/null +++ b/src/models/scheduledItem.ts @@ -0,0 +1,7 @@ +import { ScheduledItem } from "../types"; + +export const empty: ScheduledItem = { + id: "empty", + cronExpression: "* * * * *", + command: "" +}; diff --git a/src/types/ScheduledItem.ts b/src/types/ScheduledItem.ts new file mode 100644 index 0000000..19a6d8b --- /dev/null +++ b/src/types/ScheduledItem.ts @@ -0,0 +1,5 @@ +export interface ScheduledItem { + id: string; + cronExpression: string; + command: string; +} diff --git a/src/types/index.ts b/src/types/index.ts index f09871c..a8b0f75 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,4 +9,5 @@ export { ParseResult } from "./ParseResult"; export { default as PromptType } from "./PromptType"; export { default as PromptBody } from "./PromptBody"; export { default as SshConfig } from "./SshConfig"; +export { ScheduledItem } from "./ScheduledItem"; export { StringComparison } from "./StringComparison"; diff --git a/test/cronParser.test.ts b/test/cronParser.test.ts deleted file mode 100644 index 06e4475..0000000 --- a/test/cronParser.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - parseCronTab, - parseSchedule, - CronSchedule -} from "../src/lib/cronParser"; - -const daily = "0 0 * * *"; - -describe("cronParser", () => { - describe("asCronTab", () => { - it("can parse string", () => { - expect(parseCronTab("Daily").value).toBe(daily); - expect(parseCronTab("daily").value).toBe(daily); - }); - - it("can parse cron schedule", () => { - expect(parseSchedule(daily).value).toBe(CronSchedule.Daily); - }); - }); -});