Skip to content

Commit 3b68918

Browse files
committed
feat(@angular/cli): add ng completion init
`ng completion init` sets up Angular CLI autocompletion for the current user by appending `source <(ng completion)` to their `~/.bashrc`, `~/.bash_profile`, `~/.zshrc`, `~/.zsh_profile`, or `~/.profile`. I couldn't find a good way of testing an error when writing to the `~/.bashrc` file. Since the CLI checks if it has access to the file first, that would usually fail in any circumstance when the file can't be written to. Things could change in between (user modifies file permissions or disk runs out of storage), but there's no easy hook to simulate this change in the e2e test. Refs #23003.
1 parent cc720c6 commit 3b68918

File tree

5 files changed

+531
-59
lines changed

5 files changed

+531
-59
lines changed

packages/angular/cli/src/commands/completion/cli.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,57 @@
88

99
import { join } from 'path';
1010
import yargs, { Argv } from 'yargs';
11-
import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module';
11+
import {
12+
CommandModule,
13+
CommandModuleImplementation,
14+
CommandScope,
15+
} from '../../command-builder/command-module';
16+
import { addCommandModuleToYargs } from '../../command-builder/utilities/command';
17+
import { colors } from '../../utilities/color';
18+
import { initializeAutocomplete } from '../../utilities/completion';
1219

1320
export class CompletionCommandModule extends CommandModule implements CommandModuleImplementation {
1421
command = 'completion';
1522
describe = 'Generate a bash and zsh real-time type-ahead autocompletion script.';
1623
longDescriptionPath = join(__dirname, 'long-description.md');
1724

1825
builder(localYargs: Argv): Argv {
19-
return localYargs;
26+
return addCommandModuleToYargs(localYargs, CompletionInitCommandModule, this.context);
2027
}
2128

2229
run(): void {
2330
yargs.showCompletionScript();
2431
}
2532
}
33+
34+
class CompletionInitCommandModule extends CommandModule implements CommandModuleImplementation {
35+
command = 'init';
36+
describe = 'Set up Angular CLI autocompletion for your terminal.';
37+
longDescriptionPath = undefined;
38+
static override scope = CommandScope.Both;
39+
40+
builder(localYargs: Argv): Argv {
41+
return localYargs;
42+
}
43+
44+
async run(): Promise<number> {
45+
let rcFile: string;
46+
try {
47+
rcFile = await initializeAutocomplete();
48+
} catch (err) {
49+
this.context.logger.error(err.message);
50+
51+
return 1;
52+
}
53+
54+
this.context.logger.info(
55+
`
56+
Appended \`source <(ng completion)\` to \`${rcFile}\`. Restart your terminal or run the following to autocomplete \`ng commands\`:
57+
58+
${colors.yellow('source <(ng completion)')}
59+
`.trim(),
60+
);
61+
62+
return 0;
63+
}
64+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
To enable bash and zsh real-time type-ahead autocompletion, copy and paste the generated script to your `.bashrc`, `.bash_profile`, `.zshrc` or `.zsh_profile`.
1+
To enable Bash and Zsh real-time type-ahead autocompletion, run
2+
`ng completion init` and restart your terminal.
3+
4+
Alternatively, append `source <(ng completion)` to the appropriate `.bashrc`,
5+
`.bash_profile`, `.zshrc`, `.zsh_profile`, or `.profile` file.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { promises as fs } from 'fs';
10+
import * as path from 'path';
11+
import { env } from 'process';
12+
13+
/**
14+
* Sets up autocompletion for the user's terminal. This attempts to find the configuration file for
15+
* the current shell (`.bashrc`, `.zshrc`, etc.) and append a command which enables autocompletion
16+
* for the Angular CLI. Supports only Bash and Zsh. Returns whether or not it was successful.
17+
* @return The full path of the configuration file modified.
18+
*/
19+
export async function initializeAutocomplete(): Promise<string> {
20+
// Get the currently active `$SHELL` and `$HOME` environment variables.
21+
const shell = env['SHELL'];
22+
if (!shell) {
23+
throw new Error(
24+
'`$SHELL` environment variable not set. Angular CLI autocompletion only supports Bash or' +
25+
' Zsh.',
26+
);
27+
}
28+
const home = env['HOME'];
29+
if (!home) {
30+
throw new Error(
31+
'`$HOME` environment variable not set. Setting up autocompletion modifies configuration files' +
32+
' in the home directory and must be set.',
33+
);
34+
}
35+
36+
// Get all the files we can add `ng completion` to which apply to the user's `$SHELL`.
37+
const runCommandCandidates = getShellRunCommandCandidates(shell, home);
38+
if (!runCommandCandidates) {
39+
throw new Error(
40+
`Unknown \`$SHELL\` environment variable value (${shell}). Angular CLI autocompletion only supports Bash or Zsh.`,
41+
);
42+
}
43+
44+
// Get the first file that already exists or fallback to a new file of the first candidate.
45+
const candidates = await Promise.allSettled(
46+
runCommandCandidates.map((rcFile) => fs.access(rcFile).then(() => rcFile)),
47+
);
48+
const rcFile =
49+
candidates.find(
50+
(result): result is PromiseFulfilledResult<string> => result.status === 'fulfilled',
51+
)?.value ?? runCommandCandidates[0];
52+
53+
// Append Angular autocompletion setup to RC file.
54+
try {
55+
await fs.appendFile(
56+
rcFile,
57+
'\n\n# Load Angular CLI autocompletion.\nsource <(ng completion)\n',
58+
);
59+
} catch (err) {
60+
throw new Error(`Failed to append autocompletion setup to \`${rcFile}\`:\n${err.message}`);
61+
}
62+
63+
return rcFile;
64+
}
65+
66+
/** Returns an ordered list of possibile candidates of RC files used by the given shell. */
67+
function getShellRunCommandCandidates(shell: string, home: string): string[] | undefined {
68+
if (shell.toLowerCase().includes('bash')) {
69+
return ['.bashrc', '.bash_profile', '.profile'].map((file) => path.join(home, file));
70+
} else if (shell.toLowerCase().includes('zsh')) {
71+
return ['.zshrc', '.zsh_profile', '.profile'].map((file) => path.join(home, file));
72+
} else {
73+
return undefined;
74+
}
75+
}

0 commit comments

Comments
 (0)