Skip to content

Commit 28f925a

Browse files
committed
#297: CustomShell: When parsing shell command wrapper, do not parse the command content with it.
Instead, join them together later. This avoids parsing shell command content twice.
1 parent 2f697a2 commit 28f925a

File tree

11 files changed

+83
-61
lines changed

11 files changed

+83
-61
lines changed

src/ShellCommandExecutor.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,19 @@ export class ShellCommandExecutor {
152152
if (await parsing_process.processRest()) {
153153
// Parsing the rest of the variables succeeded
154154
// Execute the shell command.
155+
// TODO: Create a new class ShellCommandParsingProcess (extends ParsingProcess) whose .getParsingResults() returns the shell_command_parsing_result below. I.e. extract the parsing result conversion logic to a separate class. Note that the class should only accept shell_command_parsing_map (defined in TShellCommand.ts) as it's original_contents parameter/property.
155156
const parsing_results = parsing_process.getParsingResults();
157+
const unwrappedShellCommandContent: string = (parsing_results.shellCommandContent as ParsingResult).parsed_content as string;
156158
const shell_command_parsing_result: ShellCommandParsingResult = {
157-
unwrappedShellCommandContent: (parsing_results.unwrappedShellCommandContent as ParsingResult).parsed_content as string,
158-
wrappedShellCommandContent: (parsing_results.wrappedShellCommandContent as ParsingResult).parsed_content as string,
159+
unwrappedShellCommandContent: unwrappedShellCommandContent,
160+
wrappedShellCommandContent: parsing_results.shellCommandWrapper?.parsed_content
161+
? TShellCommand.wrapShellCommandContent(
162+
this.plugin,
163+
unwrappedShellCommandContent,
164+
parsing_results.shellCommandWrapper.parsed_content as string,
165+
)
166+
: unwrappedShellCommandContent // No wrapper, use unwrapped shell command content as wrapped.
167+
,
159168
alias: (parsing_results["alias"] as ParsingResult).parsed_content as string,
160169
environment_variable_path_augmentation: (parsing_results.environment_variable_path_augmentation as ParsingResult).parsed_content as string,
161170
stdinContent: parsing_results.stdinContent?.parsed_content as string,

src/TShellCommand.ts

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import {getSC_Events} from "./events/SC_EventList";
3030
import {debugLog} from "./Debug";
3131
import {Command} from "obsidian";
3232
import {VariableSet} from "./variables/loadVariables";
33-
import {getUsedVariables} from "./variables/parseVariables";
33+
import {
34+
getUsedVariables,
35+
parseVariableSynchronously,
36+
} from "./variables/parseVariables";
3437
import {
3538
createPreaction,
3639
CustomVariable,
@@ -53,6 +56,7 @@ import {OutputStream} from "./output_channels/OutputChannelCode";
5356
import {OutputWrapper} from "./models/output_wrapper/OutputWrapper";
5457
import {Shell} from "./shells/Shell";
5558
import {getShell} from "./shells/ShellFunctions";
59+
import {Variable_ShellCommandContent} from "./variables/Variable_ShellCommandContent";
5660

5761
export interface TShellCommandContainer {
5862
[key: string]: TShellCommand,
@@ -124,7 +128,7 @@ export class TShellCommand {
124128
*
125129
* Does not include any possible Shell provided augmentations to the shell command content.
126130
*/
127-
public getUnwrappedShellCommandContent(): string {
131+
public getShellCommandContent(): string {
128132
// Check if the shell command has defined a specific command for this operating system.
129133
const platformSpecificShellCommand: string | undefined = this.configuration.platform_specific_commands[getOperatingSystem()];
130134
if (undefined === platformSpecificShellCommand) {
@@ -137,18 +141,6 @@ export class TShellCommand {
137141
}
138142
}
139143

140-
/**
141-
* Returns a shell command string specific for the current operating system, or a generic shell command if this shell
142-
* command does not have an explicit version for the current OS.
143-
*
144-
* Includes possible Shell provided augmentations to the shell command content.
145-
*
146-
* @private Can be made public if needed.
147-
*/
148-
private getWrappedShellCommandContent(): string {
149-
return this.getShell().wrapShellCommandContent(this.getUnwrappedShellCommandContent());
150-
}
151-
152144
/**
153145
* Returns a version of the shell command that should be used if no platform specific command is defined for the
154146
* current platform. If you plan to use this for execution, consider using getShellCommand() instead, as it takes the
@@ -202,7 +194,7 @@ export class TShellCommand {
202194
* TODO: Use this method in all places where similar logic is needed. I guess generateObsidianCommandName() is the only place left.
203195
*/
204196
public getAliasOrShellCommand(): string {
205-
return this.configuration.alias || this.getUnwrappedShellCommandContent(); // TODO: Use this.getAlias().
197+
return this.configuration.alias || this.getShellCommandContent(); // TODO: Use this.getAlias().
206198
}
207199

208200
public getConfirmExecution() {
@@ -442,8 +434,8 @@ export class TShellCommand {
442434
return new ParsingProcess<shell_command_parsing_map>(
443435
this.plugin,
444436
{
445-
unwrappedShellCommandContent: this.getUnwrappedShellCommandContent(),
446-
wrappedShellCommandContent: this.getWrappedShellCommandContent(),
437+
shellCommandContent: this.getShellCommandContent(),
438+
shellCommandWrapper: this.getShell().getShellCommandWrapper(),
447439
alias: this.getAlias(),
448440
environment_variable_path_augmentation: getPATHAugmentation(this.plugin) ?? "",
449441
stdinContent: this.configuration.input_contents.stdin ?? undefined,
@@ -567,7 +559,16 @@ export class TShellCommand {
567559

568560
// Get a list CustomVariables that the shell command uses.
569561
const custom_variables = new VariableSet();
570-
const readVariablesFrom = this.getWrappedShellCommandContent(); // FIXME: Should actually include also other stuff that uses variables when executing shell commands, e.g. output wrappers. I think the best solution would be to call TShellCommand.createParsingProcess() and then get all variables from all the parseable content. Afterwards, delete the parsing process without actually parsing it, as the result would not be needed anyway. ParsingProcess class could have a new method named .getUsedVariables() that would wrap the global getUsedVariables() function and get variables used in that particular ParsingProcess.
562+
const shellCommandWrapper: string | undefined = this.getShell().getShellCommandWrapper();
563+
const readVariablesFrom = shellCommandWrapper
564+
? TShellCommand.wrapShellCommandContent(
565+
this.plugin,
566+
this.getShellCommandContent(),
567+
shellCommandWrapper,
568+
)
569+
: this.getShellCommandContent() // Use unwrapped content.
570+
;
571+
// FIXME: readVariablesFrom should actually include also other stuff that uses variables when executing shell commands, e.g. output wrappers. I think the best solution would be to call TShellCommand.createParsingProcess() and then get all variables from all the parseable content. Afterwards, delete the parsing process without actually parsing it, as the result would not be needed anyway. ParsingProcess class could have a new method named .getUsedVariables() that would wrap the global getUsedVariables() function and get variables used in that particular ParsingProcess.
571572
for (const custom_variable of getUsedVariables(this.plugin, readVariablesFrom)) {
572573
// Check that the variable IS a CustomVariable.
573574
if (custom_variable instanceof CustomVariable) { // TODO: Remove the check and pass only a list of CustomVariables to getUsedVariables().
@@ -613,6 +614,31 @@ export class TShellCommand {
613614
}
614615
return t_shell_commands[this_index - 1];
615616
}
617+
618+
/**
619+
* Replaces all occurrences of {{shell_command_content}} in shellCommandWrapper with shellCommandContent.
620+
*
621+
* @param plugin
622+
* @param shellCommandContent
623+
* @param shellCommandWrapper
624+
*/
625+
public static wrapShellCommandContent(plugin: SC_Plugin, shellCommandContent: string, shellCommandWrapper: string) {
626+
const debugMessageBase = `${this.constructor.name}.wrapShellCommandContent(): `;
627+
debugLog(`${debugMessageBase}Using wrapper: ${shellCommandWrapper} for shell command: ${shellCommandContent}`);
628+
629+
// Wrap the shell command.
630+
const wrapperParsingResult = parseVariableSynchronously(
631+
shellCommandWrapper,
632+
new Variable_ShellCommandContent(plugin, shellCommandContent),
633+
);
634+
if (!wrapperParsingResult.succeeded) {
635+
// {{shell_command_content}} is so simple that there should be no way for its parsing to fail.
636+
throw new Error("{{shell_command_content}} parsing failed, although it should not fail.");
637+
}
638+
639+
debugLog(`${debugMessageBase}Wrapped shell command: ${wrapperParsingResult.parsed_content}`);
640+
return wrapperParsingResult.parsed_content as string; // It's always string at this point, as .succeeded is checked above.
641+
}
616642
}
617643

618644
export class TShellCommandMap extends Map<string, TShellCommand> {}
@@ -637,8 +663,8 @@ export interface ShellCommandParsingResult {
637663
export type ShellCommandParsingProcess = ParsingProcess<shell_command_parsing_map>;
638664

639665
type shell_command_parsing_map = {
640-
unwrappedShellCommandContent: string,
641-
wrappedShellCommandContent: string,
666+
shellCommandContent: string,
667+
shellCommandWrapper?: string,
642668
alias: string,
643669
environment_variable_path_augmentation: string,
644670
stdinContent?: string,

src/events/SC_MenuEvent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export abstract class SC_MenuEvent extends SC_WorkspaceEvent {
6666
// Parsing succeeded.
6767
const parsing_results = parsing_process.getParsingResults();
6868
const aliasParsingResult: ParsingResult = parsing_results["alias"] as ParsingResult; // as ParsingResult: Tells TypeScript that the object exists.
69-
const unwrappedShellCommandParsingResult: ParsingResult = parsing_results.unwrappedShellCommandContent as ParsingResult; // as ParsingResult: Tells TypeScript that the object exists.
69+
const unwrappedShellCommandParsingResult: ParsingResult = parsing_results.shellCommandContent as ParsingResult; // as ParsingResult: Tells TypeScript that the object exists.
7070
title = aliasParsingResult.parsed_content || unwrappedShellCommandParsingResult.parsed_content as string; // Try to use a parsed alias, but if no alias is available, use a (short, unwrapped) parsed shell command instead. as string = parsed shell command always exist when the parsing itself has succeeded.
7171
debugLog(debugLogBaseMessage + "Menu title parsing succeeded. Will use title: " + title);
7272
menuItem.setTitle(title);

src/main.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,10 @@ export default class SC_Plugin extends Plugin {
330330
// Try to process variables that can be processed before performing preactions.
331331
await parsing_process.process();
332332
}
333-
if (parsing_process.getParsingResults().wrappedShellCommandContent?.succeeded) { // .wrappedShellCommandContent should always be present (even if parsing did not succeed), but if it's not, show errors in the else block.
333+
const parsingResults = parsing_process.getParsingResults();
334+
const shellCommandContentParsingSucceeded = parsingResults.shellCommandContent?.succeeded; // .shellCommandContent should always be present (even if parsing did not succeed), but if it's not, show errors in the else block.
335+
const shellCommandWrapperParsingSucceeded = parsingResults.shellCommandWrapper ? parsingResults.shellCommandWrapper.succeeded : true; // If no wrapper is present, pass.
336+
if (shellCommandContentParsingSucceeded && shellCommandWrapperParsingSucceeded) { // FIXME: This should not rely on just one (or two) content's parsing result, it should check all of them. Use parsing_process.getErrorMessages().length === 0 to check all parsed content.
334337
// The command was parsed correctly.
335338
const executor_instance = new ShellCommandExecutor( // Named 'executor_instance' because 'executor' is another constant.
336339
this,
@@ -348,7 +351,7 @@ export default class SC_Plugin extends Plugin {
348351
// Register an Obsidian command
349352
const obsidian_command: Command = {
350353
id: this.generateObsidianCommandId(shell_command_id),
351-
name: generateObsidianCommandName(this, t_shell_command.getUnwrappedShellCommandContent(), t_shell_command.getAlias()), // Will be overridden in command palette, but this will probably show up in hotkey settings panel - at least if command palette has not been opened yet since launching Obsidian. Also note that on some systems async variable parsing might make name generation take so long that after the name is updated in the Command object, it will not reflect in the visual menu anymore. This has happened at least on File menu on macOS, so I suspect it might concern Command palette, too. See GitHub #313 / #314 for more information. As this early name setting has been in place from the very beginning of the SC plugin, it (according to my knowledge) has protected the command palette from having similar problems that context menus have had.
354+
name: generateObsidianCommandName(this, t_shell_command.getShellCommandContent(), t_shell_command.getAlias()), // Will be overridden in command palette, but this will probably show up in hotkey settings panel - at least if command palette has not been opened yet since launching Obsidian. Also note that on some systems async variable parsing might make name generation take so long that after the name is updated in the Command object, it will not reflect in the visual menu anymore. This has happened at least on File menu on macOS, so I suspect it might concern Command palette, too. See GitHub #313 / #314 for more information. As this early name setting has been in place from the very beginning of the SC plugin, it (according to my knowledge) has protected the command palette from having similar problems that context menus have had.
352355
// Use 'checkCallback' instead of normal 'callback' because we also want to get called when the command palette is opened.
353356
checkCallback: (is_opening_command_palette): boolean | void => { // If is_opening_command_palette is true, then the return type is boolean, otherwise void.
354357
if (is_opening_command_palette) {
@@ -373,7 +376,7 @@ export default class SC_Plugin extends Plugin {
373376
// Rename Obsidian command
374377
const parsingResults = parsing_process.getParsingResults();
375378
/** Don't confuse this name with ShellCommandParsingResult interface! The properties are very different. TODO: Rename ShellCommandParsingResult to something else. */
376-
const shellCommandParsingResult: ParsingResult = parsingResults.unwrappedShellCommandContent as ParsingResult; // Use 'as' to denote that properties exist on this line and below.
379+
const shellCommandParsingResult: ParsingResult = parsingResults.shellCommandContent as ParsingResult; // Use 'as' to denote that properties exist on this line and below.
377380
const aliasParsingResult: ParsingResult = parsingResults["alias"] as ParsingResult;
378381
const parsedShellCommand: string = shellCommandParsingResult.parsed_content as string;
379382
const parsedAlias: string = aliasParsingResult.parsed_content as string;
@@ -383,13 +386,13 @@ export default class SC_Plugin extends Plugin {
383386
this.cached_parsing_processes[t_shell_command.getId()] = parsing_process;
384387
} else {
385388
// Parsing failed, so use unparsed t_shell_command.getShellCommand() and t_shell_command.getAlias().
386-
t_shell_command.renameObsidianCommand(t_shell_command.getUnwrappedShellCommandContent(), t_shell_command.getAlias());
389+
t_shell_command.renameObsidianCommand(t_shell_command.getShellCommandContent(), t_shell_command.getAlias());
387390
this.cached_parsing_processes[t_shell_command.getId()] = undefined;
388391
}
389392
});
390393
} else {
391394
// Parsing is disabled, so use unparsed t_shell_command.getShellCommand() and t_shell_command.getAlias().
392-
t_shell_command.renameObsidianCommand(t_shell_command.getUnwrappedShellCommandContent(), t_shell_command.getAlias());
395+
t_shell_command.renameObsidianCommand(t_shell_command.getShellCommandContent(), t_shell_command.getAlias());
393396
this.cached_parsing_processes[t_shell_command.getId()] = undefined;
394397
}
395398

src/models/custom_shell/CustomShellSettingsModal.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {SettingFieldGroup} from "../../settings/SC_MainSettingsTab";
5858
import * as path from "path";
5959
import * as fs from "fs";
6060
import {getPATHEnvironmentVariableName} from "../../settings/setting_elements/PathEnvironmentVariableFunctions";
61+
import {TShellCommand} from "../../TShellCommand";
6162

6263
export class CustomShellSettingsModal extends SC_Modal {
6364

@@ -484,9 +485,17 @@ export class CustomShellSettingsModal extends SC_Modal {
484485
this.plugin.newError("The test shell command is empty.");
485486
return;
486487
}
488+
const wrappedShellCommandContent: string = customShellConfiguration.shell_command_wrapper
489+
? TShellCommand.wrapShellCommandContent(
490+
this.plugin,
491+
customShellConfiguration.shell_command_test,
492+
customShellConfiguration.shell_command_wrapper,
493+
)
494+
: customShellConfiguration.shell_command_test // No wrapper, use unwrapped shell command content.
495+
;
487496
const testShellCommandParsingResult: ParsingResult = await parseVariables(
488497
this.plugin,
489-
this.getCustomShell().wrapShellCommandContent(customShellConfiguration.shell_command_test),
498+
wrappedShellCommandContent,
490499
this.getCustomShell(),
491500
true, // Enable escaping, but if this.customShellInstance.configuration.escaper is "none", then escaping is prevented anyway.
492501
null, // No TShellCommand, so no access for default values.

0 commit comments

Comments
 (0)