diff --git a/README.md b/README.md
index bd127fe..fa851d1 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,29 @@
# Bash Options
-Provide the options to export a string from bash command as variable
+A VS Code extension offering a suite of refactoring and utility tools for Shell/Bash scripting.
+
-
\ No newline at end of file
+## Features
+
+This extension provides 10 powerful features to speed up your shell scripting workflow:
+
+### Refactoring
+1. **Extract Variable (Interactive)**: Select a string and convert it into an exported variable. Prompts for the variable name.
+2. **Extract Function**: Select a block of code and wrap it into a function. Prompts for the function name and places the function definition at the top of the file.
+3. **Toggle Quotes**: Switch between single (`'`) and double (`"`) quotes for the selected text.
+4. **Toggle Parameter Expansion**: Switch between simple (`$VAR`) and brace (`${VAR}`) parameter expansion.
+5. **Convert to HereDoc**: Convert the selected text into a `cat < {
+ editBuilder.insert(insertPos, `${indent}${echoStmt}\n`);
+ });
+}
diff --git a/src/commands/extractFunction.ts b/src/commands/extractFunction.ts
new file mode 100644
index 0000000..5984872
--- /dev/null
+++ b/src/commands/extractFunction.ts
@@ -0,0 +1,53 @@
+import * as vscode from 'vscode';
+
+export async function extractFunction() {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+ const selection = editor.selection;
+ if (selection.isEmpty) {
+ return;
+ }
+
+ const text = editor.document.getText(selection);
+
+ const functionName = await vscode.window.showInputBox({
+ prompt: 'Enter function name'
+ });
+
+ if (!functionName) {
+ return;
+ }
+
+ // Find a good place to insert the function.
+ // Ideally at the top of the file, after shebang/comments, or before the current block.
+ // Let's put it before the current "block" if we can determine indentation, or just before current line?
+ // Or just at the top (after shebang).
+
+ // Strategy: Insert at top of file, after shebang.
+ // If no shebang, insert at top.
+
+ const doc = editor.document;
+ let insertLine = 0;
+ if (doc.lineAt(0).text.startsWith('#!')) {
+ insertLine = 1;
+ // Skip subsequent comments?
+ while (insertLine < doc.lineCount && doc.lineAt(insertLine).text.startsWith('#')) {
+ insertLine++;
+ }
+ // Skip empty lines
+ while (insertLine < doc.lineCount && doc.lineAt(insertLine).isEmptyOrWhitespace) {
+ insertLine++;
+ }
+ }
+
+ const insertPos = new vscode.Position(insertLine, 0);
+
+ const functionCode = `${functionName}() {\n${text}\n}\n\n`;
+
+ editor.edit(editBuilder => {
+ editBuilder.insert(insertPos, functionCode);
+ editBuilder.replace(selection, functionName);
+ });
+}
diff --git a/src/commands/extractVariable.ts b/src/commands/extractVariable.ts
new file mode 100644
index 0000000..2d6edf0
--- /dev/null
+++ b/src/commands/extractVariable.ts
@@ -0,0 +1,85 @@
+import * as vscode from 'vscode';
+
+export async function extractVariable() {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+
+ const selection = editor.selection;
+ if (selection.isEmpty) {
+ return;
+ }
+
+ let line = editor.document.lineAt(selection.start.line);
+ const text = editor.document.getText(selection);
+
+ if (line.lineNumber > 0) {
+ // Find previous line which does not end with a backslash
+ // Loop backwards to find the start of the command if it's multi-line
+ let prevLineNum = line.lineNumber - 1;
+ while (prevLineNum >= 0) {
+ const prevLine = editor.document.lineAt(prevLineNum);
+ if (!prevLine.text.endsWith('\\')) {
+ break;
+ }
+ prevLineNum--;
+ }
+ // The insert position should be after that "clean" line (so at prevLineNum + 1)
+ // Original logic:
+ // line = editor.document.lineAt(line.lineNumber - 1);
+ // while (line.text.endsWith('\\')) ...
+ // line = editor.document.lineAt(line.lineNumber + 1);
+
+ // Let's replicate the original logic carefully but safely
+ let checkLine = editor.document.lineAt(line.lineNumber - 1);
+ while (checkLine.lineNumber > 0 && checkLine.text.endsWith('\\')) {
+ checkLine = editor.document.lineAt(checkLine.lineNumber - 1);
+ }
+ // If we stopped because no backslash, we want the line AFTER that one.
+ // If we stopped because lineNumber 0, we check if it has backslash?
+ // Actually, the logic is "Find the start of the current command block".
+ // The original logic:
+ // line = editor.document.lineAt(line.lineNumber - 1);
+ // while (line.text.endsWith('\\')) { line = editor.document.lineAt(line.lineNumber - 1); }
+ // line = editor.document.lineAt(line.lineNumber + 1);
+
+ // If line 0 has backslash, loop might crash if not careful with indexes.
+ // But original code didn't check >0 inside while.
+
+ // Improved logic:
+ let insertLineIndex = line.lineNumber;
+ if (insertLineIndex > 0) {
+ let i = insertLineIndex - 1;
+ while(i >= 0) {
+ const l = editor.document.lineAt(i);
+ if (!l.text.trim().endsWith('\\')) {
+ break;
+ }
+ i--;
+ }
+ insertLineIndex = i + 1;
+ }
+
+ line = editor.document.lineAt(insertLineIndex);
+ }
+
+ const position = new vscode.Position(line.lineNumber, 0);
+
+ // Prompt for variable name (Batch 3 feature, but implementing now as we are refactoring)
+ const variableName = await vscode.window.showInputBox({
+ prompt: 'Enter variable name',
+ value: 'var_' + text.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()
+ });
+
+ if (!variableName) {
+ return;
+ }
+
+ editor.edit(editBuilder => {
+ // Escape single quotes in the text
+ const escapedText = text.replace(/'/g, "'\\''");
+ editBuilder.insert(position, `export ${variableName}='${escapedText}'\n`);
+ editBuilder.replace(selection, `$${variableName}`);
+ });
+}
diff --git a/src/commands/insertShebang.ts b/src/commands/insertShebang.ts
new file mode 100644
index 0000000..8dc375c
--- /dev/null
+++ b/src/commands/insertShebang.ts
@@ -0,0 +1,20 @@
+import * as vscode from 'vscode';
+
+export async function insertShebang() {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+
+ const doc = editor.document;
+ const firstLine = doc.lineAt(0);
+
+ if (firstLine.text.startsWith('#!')) {
+ vscode.window.showInformationMessage('Shebang already exists.');
+ return;
+ }
+
+ editor.edit(editBuilder => {
+ editBuilder.insert(new vscode.Position(0, 0), '#!/bin/bash\n');
+ });
+}
diff --git a/src/commands/makeExecutable.ts b/src/commands/makeExecutable.ts
new file mode 100644
index 0000000..0cceaab
--- /dev/null
+++ b/src/commands/makeExecutable.ts
@@ -0,0 +1,22 @@
+import * as vscode from 'vscode';
+import * as fs from 'fs';
+
+export async function makeExecutable() {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+
+ const doc = editor.document;
+ if (doc.isUntitled) {
+ vscode.window.showWarningMessage('Please save the file first.');
+ return;
+ }
+
+ try {
+ await fs.promises.chmod(doc.fileName, 0o755);
+ vscode.window.showInformationMessage(`Marked "${doc.fileName}" as executable.`);
+ } catch (error) {
+ vscode.window.showErrorMessage(`Failed to make executable: ${error}`);
+ }
+}
diff --git a/src/commands/toggleParameterExpansion.ts b/src/commands/toggleParameterExpansion.ts
new file mode 100644
index 0000000..0036ad8
--- /dev/null
+++ b/src/commands/toggleParameterExpansion.ts
@@ -0,0 +1,75 @@
+import * as vscode from 'vscode';
+
+export async function toggleParameterExpansion() {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+
+ const selection = editor.selection;
+ let range = new vscode.Range(selection.start, selection.end);
+ let text = editor.document.getText(range);
+
+ // If selection is empty, try to get word under cursor
+ if (selection.isEmpty) {
+ const wordRange = editor.document.getWordRangeAtPosition(selection.active);
+ if (wordRange) {
+ range = wordRange;
+ text = editor.document.getText(range);
+ }
+ }
+
+ // Check surrounding characters for $ and {}
+ // We need to look a bit wider if the selection is just the var name.
+
+ // Simplest approach: Text provided is the variable name (e.g. VAR) or $VAR or ${VAR}
+
+ // Case 1: ${VAR} -> $VAR
+ if (text.startsWith('${') && text.endsWith('}')) {
+ const content = text.substring(2, text.length - 1);
+ // Ensure content is a valid identifier for simple expansion
+ if (/^[a-zA-Z0-9_]+$/.test(content)) {
+ editor.edit(editBuilder => {
+ editBuilder.replace(range, `$${content}`);
+ });
+ return;
+ }
+ }
+
+ // Case 2: $VAR -> ${VAR}
+ if (text.startsWith('$')) {
+ const content = text.substring(1);
+ if (!content.startsWith('{')) {
+ editor.edit(editBuilder => {
+ editBuilder.replace(range, `\${${content}}`);
+ });
+ return;
+ }
+ }
+
+ // Case 3: VAR -> ${VAR} (Assuming user selected just the name)
+ if (/^[a-zA-Z0-9_]+$/.test(text)) {
+ // check if preceded by $
+ const startPos = range.start;
+ if (startPos.character > 0) {
+ const prevCharRange = new vscode.Range(startPos.translate(0, -1), startPos);
+ const prevChar = editor.document.getText(prevCharRange);
+ if (prevChar === '$') {
+ // It is $VAR, convert to ${VAR}
+ // We need to replace $VAR with ${VAR}
+ const fullRange = new vscode.Range(startPos.translate(0, -1), range.end);
+ editor.edit(editBuilder => {
+ editBuilder.replace(fullRange, `\${${text}}`);
+ });
+ return;
+ }
+ }
+
+ // If not preceded by $, assume it's just a string, wrap it?
+ // Maybe user wants to turn `echo var` into `echo ${var}`?
+ // Let's assume they want to turn a bare word into a variable expansion.
+ editor.edit(editBuilder => {
+ editBuilder.replace(range, `\${${text}}`);
+ });
+ }
+}
diff --git a/src/commands/toggleQuotes.ts b/src/commands/toggleQuotes.ts
new file mode 100644
index 0000000..185ff4b
--- /dev/null
+++ b/src/commands/toggleQuotes.ts
@@ -0,0 +1,61 @@
+import * as vscode from 'vscode';
+
+export async function toggleQuotes() {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+
+ const selection = editor.selection;
+ if (selection.isEmpty) {
+ // Try to find if cursor is inside quotes?
+ // For simplicity, let's require selection for now, or just select the word.
+ // If empty, let's select current word.
+ const range = editor.document.getWordRangeAtPosition(selection.active);
+ if (!range) {
+ return;
+ }
+ // Check if the range is surrounded by quotes
+ const start = range.start;
+ const end = range.end;
+ // Expand to check quotes?
+ // This is getting complex without parsing.
+ // Let's stick to "Selection Required" or operate on word if simple.
+ return;
+ }
+
+ const text = editor.document.getText(selection);
+
+ // Check if whole text is single quoted
+ if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) {
+ const content = text.substring(1, text.length - 1);
+ // Convert to double quotes
+ // Escape existing double quotes
+ const newContent = content.replace(/"/g, '\\"');
+ editor.edit(editBuilder => {
+ editBuilder.replace(selection, `"${newContent}"`);
+ });
+ return;
+ }
+
+ // Check if whole text is double quoted
+ if (text.startsWith('"') && text.endsWith('"') && text.length >= 2) {
+ const content = text.substring(1, text.length - 1);
+ // Convert to single quotes
+ // Unescape escaped double quotes
+ const unescaped = content.replace(/\\"/g, '"');
+ // Escape single quotes (Bash style: ' -> '"'"')
+ const newContent = unescaped.replace(/'/g, `'"'"'`);
+ editor.edit(editBuilder => {
+ editBuilder.replace(selection, `'${newContent}'`);
+ });
+ return;
+ }
+
+ // If not quoted, wrap in double quotes? Or single?
+ // Let's wrap in double quotes as default.
+ const newContent = text.replace(/"/g, '\\"');
+ editor.edit(editBuilder => {
+ editBuilder.replace(selection, `"${newContent}"`);
+ });
+}
diff --git a/src/commands/wrapInFor.ts b/src/commands/wrapInFor.ts
new file mode 100644
index 0000000..e65cf4b
--- /dev/null
+++ b/src/commands/wrapInFor.ts
@@ -0,0 +1,28 @@
+import * as vscode from 'vscode';
+
+export async function wrapInFor() {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+ const selection = editor.selection;
+ if (selection.isEmpty) {
+ return;
+ }
+
+ const text = editor.document.getText(selection);
+ // Indent the text
+ const lines = text.split('\n');
+ const tabSize = editor.options.tabSize;
+ const insertSpaces = editor.options.insertSpaces;
+ const indent = insertSpaces ? ' '.repeat(Number(tabSize)) : '\t';
+ const indentedText = lines.map(l => indent + l).join('\n');
+
+ const snippet = new vscode.SnippetString(
+ 'for ${1:i} in ${2:list}; do\n' +
+ indentedText + '\n' +
+ 'done'
+ );
+
+ editor.insertSnippet(snippet, selection);
+}
diff --git a/src/commands/wrapInIf.ts b/src/commands/wrapInIf.ts
new file mode 100644
index 0000000..d4ed5ed
--- /dev/null
+++ b/src/commands/wrapInIf.ts
@@ -0,0 +1,28 @@
+import * as vscode from 'vscode';
+
+export async function wrapInIf() {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+ const selection = editor.selection;
+ if (selection.isEmpty) {
+ return;
+ }
+
+ const text = editor.document.getText(selection);
+ // Indent the text
+ const lines = text.split('\n');
+ const tabSize = editor.options.tabSize;
+ const insertSpaces = editor.options.insertSpaces;
+ const indent = insertSpaces ? ' '.repeat(Number(tabSize)) : '\t';
+ const indentedText = lines.map(l => indent + l).join('\n');
+
+ const snippet = new vscode.SnippetString(
+ 'if [ ${1:condition} ]; then\n' +
+ indentedText + '\n' +
+ 'fi'
+ );
+
+ editor.insertSnippet(snippet, selection);
+}
diff --git a/src/extension.ts b/src/extension.ts
index 3223da3..ee3f585 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,52 +1,38 @@
import * as vscode from 'vscode';
+import { extractVariable } from './commands/extractVariable';
+import { makeExecutable } from './commands/makeExecutable';
+import { insertShebang } from './commands/insertShebang';
+import { toggleQuotes } from './commands/toggleQuotes';
+import { toggleParameterExpansion } from './commands/toggleParameterExpansion';
+import { wrapInIf } from './commands/wrapInIf';
+import { wrapInFor } from './commands/wrapInFor';
+import { convertToHereDoc } from './commands/convertToHereDoc';
+import { extractFunction } from './commands/extractFunction';
+import { debugPrint } from './commands/debugPrint';
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
- // Use the console to output diagnostic information (console.log) and errors (console.error)
- // This line of code will only be executed once when your extension is activated
- console.log('Congratulations, your extension "extension" is now active!');
-
- //Define the command for bash.extractVariable
- let bashExtractVariableCommand = vscode.commands.registerCommand('bash.extractVariable', () => {
-
- // Get complete text document
- let editor = vscode.window.activeTextEditor;
- if (editor) {
- // Get the text document's selection
- let selection = editor.selection;
- if (!selection.isEmpty) {
-
- // Get the text document's current line
- let line = editor.document.lineAt(selection.start.line);
- // Get text from the selection
- let text = editor.document.getText(selection);
-
- if(line.lineNumber > 0) {
- // Find previous line which does not end with a backslash
- line = editor.document.lineAt(line.lineNumber - 1);
- while (line.text.endsWith('\\')) {
- line = editor.document.lineAt(line.lineNumber - 1);
- }
- line = editor.document.lineAt(line.lineNumber + 1);
- }
-
- // Convert line number into postiion
- let position = new vscode.Position(line.lineNumber, 0);
-
- editor.edit(editBuilder => {
- // Generate a random variable name from the text
- let variableName = 'var_' + text.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
-
- //Add a line before the selection
- editBuilder.insert(position, `export ${variableName}='${text}'\n`);
-
- // Update the selection with $ infront of the word
- editBuilder.replace(selection, `$${variableName}`);
- });
- }
- }
- });
+ console.log('Congratulations, your extension "bash-options" is now active!');
+
+ // Register commands
+ // Batch 1 & Refactor
+ context.subscriptions.push(vscode.commands.registerCommand('bash.extractVariable', extractVariable));
+
+ // Batch 1
+ context.subscriptions.push(vscode.commands.registerCommand('bash.makeExecutable', makeExecutable));
+ context.subscriptions.push(vscode.commands.registerCommand('bash.insertShebang', insertShebang));
+ context.subscriptions.push(vscode.commands.registerCommand('bash.toggleQuotes', toggleQuotes));
+ context.subscriptions.push(vscode.commands.registerCommand('bash.toggleParameterExpansion', toggleParameterExpansion));
+
+ // Batch 2
+ context.subscriptions.push(vscode.commands.registerCommand('bash.wrapInIf', wrapInIf));
+ context.subscriptions.push(vscode.commands.registerCommand('bash.wrapInFor', wrapInFor));
+ context.subscriptions.push(vscode.commands.registerCommand('bash.convertToHereDoc', convertToHereDoc));
+
+ // Batch 3
+ context.subscriptions.push(vscode.commands.registerCommand('bash.extractFunction', extractFunction));
+ context.subscriptions.push(vscode.commands.registerCommand('bash.debugPrint', debugPrint));
}
// this method is called when your extension is deactivated
diff --git a/src/test/suite.test.ts b/src/test/suite.test.ts
new file mode 100644
index 0000000..b13f0a9
--- /dev/null
+++ b/src/test/suite.test.ts
@@ -0,0 +1,12 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+// import * as myExtension from '../../extension';
+
+suite('Extension Test Suite', () => {
+ vscode.window.showInformationMessage('Start all tests.');
+
+ test('Sample test', () => {
+ assert.strictEqual(-1, [1, 2, 3].indexOf(5));
+ assert.strictEqual(-1, [1, 2, 3].indexOf(0));
+ });
+});