/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs/promises'; import * as path from 'path'; import { cleanupText, checkWindows, execAsync, copyright } from './terminalScriptHelpers'; checkWindows(); interface ICommandDetails { description: string; args: string | undefined; shortDescription?: string; } let fishBuiltinsCommandDescriptionsCache = new Map(); // Fallback descriptions for commands that don't return proper help information const fallbackDescriptions: Record = { '[': { shortDescription: 'Test if a statement is true', description: 'Evaluate an expression and return a status of true (0) or false (non-zero). Unlike the `test` command, the `[` command requires a closing `]`.', args: 'EXPRESSION ]' }, 'break': { shortDescription: 'Exit the current loop', description: 'Terminate the execution of the nearest enclosing `while` or `for` loop and proceed with the next command after the loop.', args: undefined }, 'breakpoint': { shortDescription: 'Launch debug mode', description: 'Pause execution and launch an interactive debug prompt. This is useful for inspecting the state of a script at a specific point.', args: undefined }, 'case': { shortDescription: 'Match a value against patterns', description: 'Within a `switch` block, the `case` command specifies patterns to match against the given value, executing the associated block if a match is found.', args: 'PATTERN...' }, 'continue': { shortDescription: 'Skip to the next iteration of a loop', description: 'Within a `while` or `for` loop, `continue` skips the remaining commands in the current iteration and proceeds to the next iteration of the loop.', args: undefined }, 'else': { shortDescription: 'Execute commands if the previous condition was false', description: 'In an `if` block, the `else` section contains commands that execute if none of the preceding `if` or `else if` conditions were true.', args: undefined }, 'end': { shortDescription: 'Terminate a block of code', description: 'Conclude a block of code initiated by constructs like `if`, `switch`, `while`, `for`, or `function`.', args: undefined }, 'eval': { shortDescription: 'Execute arguments as a command', description: 'Concatenate all arguments into a single command and execute it. This allows for dynamic construction and execution of commands.', args: 'COMMAND...' }, 'false': { shortDescription: 'Return an unsuccessful result', description: 'A command that returns a non-zero exit status, indicating failure. It is often used in scripts to represent a false condition.', args: undefined }, 'realpath': { shortDescription: 'Resolve and print the absolute path', description: 'Convert each provided path to its absolute, canonical form by resolving symbolic links and relative path components.', args: 'PATH...' }, ':': { shortDescription: 'No operation command', description: 'The `:` command is a no-op (no operation) command that returns a successful (zero) exit status. It can be used as a placeholder in scripts where a command is syntactically required but no action is desired.', args: undefined }, 'test': { shortDescription: 'Evaluate conditional expressions', description: 'The `test` command evaluates conditional expressions and sets the exit status to 0 if the expression is true, and 1 if it is false. It supports various operators to evaluate expressions related to strings, numbers, and file attributes.', args: 'EXPRESSION' }, 'true': { shortDescription: 'Return a successful result', description: 'The `true` command always returns a successful (zero) exit status. It is often used in scripts and conditional statements where an unconditional success result is needed.', args: undefined }, 'printf': { shortDescription: 'Display formatted text', description: 'The `printf` command formats and prints text according to a specified format string. Unlike `echo`, `printf` does not append a newline unless explicitly included in the format.', args: 'FORMAT [ARGUMENT...]' } }; async function createCommandDescriptionsCache(): Promise { const cachedCommandDescriptions: Map = new Map(); try { // Get list of all builtins const builtinsOutput = await execAsync('fish -c "builtin -n"').then(r => r.stdout.trim()); const builtins = builtinsOutput.split('\n'); console.log(`Found ${builtins.length} Fish builtin commands`); for (const cmd of builtins) { try { // Get help info for each builtin const helpOutput = await execAsync(`fish -c "${cmd} --help 2>&1"`).then(r => r.stdout); let set = false; if (helpOutput && !helpOutput.includes('No help for function') && !helpOutput.includes('See the web documentation')) { const cleanHelpText = cleanupText(helpOutput); // Split the text into lines to process const lines = cleanHelpText.split('\n'); // Extract the short description, args, and full description const { shortDescription, args, description } = extractHelpContent(cmd, lines); cachedCommandDescriptions.set(cmd, { shortDescription, description, args }); set = description !== ''; } if (!set) { // Use fallback descriptions for commands that don't return proper help if (fallbackDescriptions[cmd]) { console.info(`Using fallback description for ${cmd}`); cachedCommandDescriptions.set(cmd, fallbackDescriptions[cmd]); } else { console.info(`No fallback description exists for ${cmd}`); } } } catch { // Use fallback descriptions for commands that throw an error if (fallbackDescriptions[cmd]) { console.info('Using fallback description for', cmd); cachedCommandDescriptions.set(cmd, fallbackDescriptions[cmd]); } else { console.info(`Error getting help for ${cmd}`); } } } } catch (e) { console.error('Error creating Fish builtins cache:', e); process.exit(1); } fishBuiltinsCommandDescriptionsCache = cachedCommandDescriptions; } /** * Extracts short description, args, and full description from help text lines */ function extractHelpContent(cmd: string, lines: string[]): { shortDescription: string; args: string | undefined; description: string } { let shortDescription = ''; let args: string | undefined; let description = ''; // Skip the first line (usually just command name and basic usage) let i = 1; // Skip any leading empty lines while (i < lines.length && lines[i].trim().length === 0) { i++; } // The next non-empty line after the command name is typically // either the short description or additional usage info const startLine = i; // Find where the short description starts if (i < lines.length) { // First, check if the line has a command prefix and remove it let firstContentLine = lines[i].trim(); const cmdPrefixRegex = new RegExp(`^${cmd}\\s*-\\s*`, 'i'); firstContentLine = firstContentLine.replace(cmdPrefixRegex, ''); // First non-empty line is the short description shortDescription = firstContentLine; i++; // Next non-empty line (after short description) is typically args while (i < lines.length && lines[i].trim().length === 0) { i++; } if (i < lines.length) { // Found a line after the short description - that's our args args = lines[i].trim(); i++; } } // Find the DESCRIPTION marker which marks the end of args section let descriptionIndex = -1; for (let j = i; j < lines.length; j++) { if (lines[j].trim() === 'DESCRIPTION') { descriptionIndex = j; break; } } // If DESCRIPTION marker is found, consider everything between i and descriptionIndex as part of args if (descriptionIndex > i) { // Combine lines from i up to (but not including) descriptionIndex const additionalArgs = lines.slice(i, descriptionIndex).join('\n').trim(); if (additionalArgs) { args = args ? `${args}\n${additionalArgs}` : additionalArgs; } i = descriptionIndex + 1; // Move past the DESCRIPTION line } // The rest is the full description (skipping any empty lines after args) while (i < lines.length && lines[i].trim().length === 0) { i++; } // Combine the remaining lines into the full description description = lines.slice(Math.max(i, startLine)).join('\n').trim(); // If description is empty, use the short description if (!description && shortDescription) { description = shortDescription; } // Extract just the first sentence for short description const firstPeriodIndex = shortDescription.indexOf('.'); if (firstPeriodIndex > 0) { shortDescription = shortDescription.substring(0, firstPeriodIndex + 1).trim(); } else if (shortDescription.length > 100) { shortDescription = shortDescription.substring(0, 100) + '...'; } return { shortDescription, args, description }; } const main = async () => { try { await createCommandDescriptionsCache(); console.log('Created Fish command descriptions cache with', fishBuiltinsCommandDescriptionsCache.size, 'entries'); // Save the cache to a TypeScript file const cacheFilePath = path.join(__dirname, '../src/shell/fishBuiltinsCache.ts'); const cacheObject = Object.fromEntries(fishBuiltinsCommandDescriptionsCache); const tsContent = `${copyright}\n\nexport const fishBuiltinsCommandDescriptionsCache = ${JSON.stringify(cacheObject, null, 2)} as const;`; await fs.writeFile(cacheFilePath, tsContent, 'utf8'); console.log('Saved Fish command descriptions cache to fishBuiltinsCache.ts with', Object.keys(cacheObject).length, 'entries'); } catch (error) { console.error('Error:', error); } }; main();