Repository: microsoft/inshellisense (ts)

Issue:
Autocomplete for Alias in .bashrc
**Is your feature request related to a problem? Please describe.**
I have so many aliases that maybe I forget, or I do have typos when typing fast.

**Describe the solution you'd like**
It is nice to have autocompletion for the listed aliases in .bashrc

**Additional context**
I'm using windows with git-bash and listing all my setting and alias in .bashrc file.
For example, I have the alias 'pyenv_cr' for creating environment 'python311 -m venv env', it is nice if the intellisense appears by I type 'pyev...'

Thanks a lot for this opensource project.

Pull request description:
feat: add alias expansion for bash & zsh
Closes #157

Patch:
diff --git a/src/commands/root.ts b/src/commands/root.ts
index 3dfdd33..1315091 100644
--- a/src/commands/root.ts
+++ b/src/commands/root.ts
@@ -7,6 +7,7 @@ import { inferShell } from "../utils/shell.js";
 import { loadConfig } from "../utils/config.js";
 import { Command } from "commander";
 import log from "../utils/log.js";
+import { loadAliases } from "../runtime/alias.js";
 
 export const supportedShells = shells.join(", ");
 
@@ -41,5 +42,6 @@ export const action = (program: Command) => async (options: RootCommandOptions)
   } else if (shell == Shell.Bash) {
     await setupBashPreExec();
   }
+  await loadAliases(shell);
   await render(shell, options.test ?? false, options.parentTermExit ?? false);
 };
diff --git a/src/runtime/alias.ts b/src/runtime/alias.ts
new file mode 100644
index 0000000..4621b6c
--- /dev/null
+++ b/src/runtime/alias.ts
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import log from "../utils/log.js";
+import { gitBashPath, Shell } from "../utils/shell.js";
+import { CommandToken, parseCommand } from "./parser.js";
+import { buildExecuteShellCommand } from "./utils.js";
+import os from "node:os";
+
+const loadedAliases: { [key: string]: CommandToken[] | undefined } = {};
+const platform = os.platform();
+const executeShellCommand = buildExecuteShellCommand(5_000);
+
+const loadBashAliases = async () => {
+  const shellTarget = platform == "win32" ? await gitBashPath() : Shell.Bash;
+  const { stdout, stderr, status } = await executeShellCommand({ command: shellTarget, args: ["-i", "-c", "alias"], cwd: process.cwd() });
+  if (status !== 0) {
+    log.debug({ msg: "failed to load bash aliases", stderr, status });
+    return;
+  }
+
+  return stdout
+    .trim()
+    .split("\n")
+    .forEach((line) => {
+      const [alias, ...commandSegments] = line.replace("alias ", "").replaceAll("'\\''", "'").split("=");
+      loadedAliases[alias] = parseCommand(commandSegments.join("=").slice(1, -1) + " ");
+    });
+};
+
+const loadZshAliases = async () => {
+  const { stdout, stderr, status } = await executeShellCommand({ command: Shell.Zsh, args: ["-i", "-c", "alias"], cwd: process.cwd() });
+  if (status !== 0) {
+    log.debug({ msg: "failed to load zsh aliases", stderr, status });
+    return;
+  }
+
+  return stdout
+    .trim()
+    .split("\n")
+    .forEach((line) => {
+      const [alias, ...commandSegments] = line.replaceAll("'\\''", "'").split("=");
+      loadedAliases[alias] = parseCommand(commandSegments.join("=").slice(1, -1) + " ");
+    });
+};
+
+export const loadAliases = async (shell: Shell) => {
+  switch (shell) {
+    case Shell.Bash:
+      await loadBashAliases();
+      break;
+    case Shell.Zsh:
+      await loadZshAliases();
+      break;
+  }
+  return [];
+};
+
+export const aliasExpand = (command: CommandToken[]): CommandToken[] => {
+  if (!command.at(0)?.complete) return command;
+
+  const alias = loadedAliases[command.at(0)?.token ?? ""];
+  if (alias) {
+    log.debug({ msg: "expanding alias", alias, command: command.slice(1) });
+    return [...alias, ...command.slice(1)];
+  }
+  return command;
+};
diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts
index 730ea39..0cae95f 100644
--- a/src/runtime/runtime.ts
+++ b/src/runtime/runtime.ts
@@ -12,6 +12,7 @@ import { getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from ".
 import { SuggestionBlob } from "./model.js";
 import { buildExecuteShellCommand, resolveCwd } from "./utils.js";
 import { Shell } from "../utils/shell.js";
+import { aliasExpand } from "./alias.js";
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any -- recursive type, setting as any
 const specSet: any = {};
@@ -61,11 +62,12 @@ const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise<Fig.Spe
 };
 
 export const getSuggestions = async (cmd: string, cwd: string, shell: Shell): Promise<SuggestionBlob | undefined> => {
-  const activeCmd = parseCommand(cmd);
+  let activeCmd = parseCommand(cmd);
   const rootToken = activeCmd.at(0);
   if (activeCmd.length === 0 || !rootToken?.complete) {
     return;
   }
+  activeCmd = aliasExpand(activeCmd);
 
   const spec = await loadSpec(activeCmd);
   if (spec == null) return;


Test patch:
diff --git a/src/tests/runtime/__snapshots__/alias.test.ts.snap b/src/tests/runtime/__snapshots__/alias.test.ts.snap
new file mode 100644
index 0000000..46aa0f5
--- /dev/null
+++ b/src/tests/runtime/__snapshots__/alias.test.ts.snap
@@ -0,0 +1,121 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`aliasExpand expand on bash aliases 1`] = `
+[
+  {
+    "complete": false,
+    "isOption": false,
+    "token": "glo",
+  },
+]
+`;
+
+exports[`aliasExpand expand on bash aliases 2`] = `
+[
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "echo",
+  },
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "'lo'",
+  },
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "'la'",
+  },
+]
+`;
+
+exports[`aliasExpand expand on bash aliases 3`] = `
+[
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "git",
+  },
+]
+`;
+
+exports[`aliasExpand expand on bash aliases 4`] = `
+[
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "ls",
+  },
+  {
+    "complete": true,
+    "isOption": true,
+    "token": "--color",
+  },
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "auto",
+  },
+]
+`;
+
+exports[`aliasExpand expand on zsh aliases 1`] = `
+[
+  {
+    "complete": false,
+    "isOption": false,
+    "token": "glo",
+  },
+]
+`;
+
+exports[`aliasExpand expand on zsh aliases 2`] = `
+[
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "echo",
+  },
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "'lo'",
+  },
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "'la'",
+  },
+]
+`;
+
+exports[`aliasExpand expand on zsh aliases 3`] = `
+[
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "git",
+  },
+]
+`;
+
+exports[`aliasExpand expand on zsh aliases 4`] = `
+[
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "ls",
+  },
+  {
+    "complete": true,
+    "isOption": true,
+    "token": "--color",
+  },
+  {
+    "complete": true,
+    "isOption": false,
+    "token": "auto",
+  },
+]
+`;
diff --git a/src/tests/runtime/alias.test.ts b/src/tests/runtime/alias.test.ts
new file mode 100644
index 0000000..01ed74a
--- /dev/null
+++ b/src/tests/runtime/alias.test.ts
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { jest } from "@jest/globals";
+import { Shell } from "../../utils/shell";
+
+const mockExecuteShellCommand = jest.fn();
+
+jest.unstable_mockModule("../../runtime/utils.js", () => ({
+  buildExecuteShellCommand: () => mockExecuteShellCommand,
+}));
+
+const { aliasExpand, loadAliases } = await import("../../runtime/alias.js");
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+describe("aliasExpand", () => {
+  test("expand on bash aliases", async () => {
+    //@ts-expect-error - jest.fn() has no implementation
+    mockExecuteShellCommand.mockResolvedValue({
+      stdout: `alias glo='git log --oneline'
+alias la='echo '\\''lo'\\'' '\\''la'\\'''
+alias ls='ls --color=auto'`,
+      status: 0,
+    });
+
+    await loadAliases(Shell.Bash);
+    expect(aliasExpand([{ token: "glo", complete: false, isOption: false }])).toMatchSnapshot();
+    expect(aliasExpand([{ token: "la", complete: true, isOption: false }])).toMatchSnapshot();
+    expect(aliasExpand([{ token: "git", complete: true, isOption: false }])).toMatchSnapshot();
+    expect(aliasExpand([{ token: "ls", complete: true, isOption: false }])).toMatchSnapshot();
+  });
+
+  test("expand on zsh aliases", async () => {
+    //@ts-expect-error - jest.fn() has no implementation
+    mockExecuteShellCommand.mockResolvedValue({
+      stdout: `glo='git log --oneline'
+la='echo '\\''lo'\\'' '\\''la'\\'''
+ls='ls --color=auto'`,
+      status: 0,
+    });
+
+    await loadAliases(Shell.Zsh);
+    expect(aliasExpand([{ token: "glo", complete: false, isOption: false }])).toMatchSnapshot();
+    expect(aliasExpand([{ token: "la", complete: true, isOption: false }])).toMatchSnapshot();
+    expect(aliasExpand([{ token: "git", complete: true, isOption: false }])).toMatchSnapshot();
+    expect(aliasExpand([{ token: "ls", complete: true, isOption: false }])).toMatchSnapshot();
+  });
+});