Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

<img src="./static/demo.gif" width="800"/>

<img src="./static/demo.gif" width="800"/>
## 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 <<EOF` HereDoc block.

### Wrappers
6. **Wrap in If**: Wrap the selected code in an `if` block (`if [ condition ]; then ... fi`).
7. **Wrap in For Loop**: Wrap the selected code in a `for` loop (`for i in list; do ... done`).

### Utilities
8. **Make Executable**: Run `chmod +x` on the current file directly from the editor.
9. **Insert Shebang**: Insert `#!/bin/bash` at the top of the file if not present.
10. **Debug Print**: Insert an `echo "DEBUG: ..."` statement for the selected variable or text to help with debugging.

## Usage

Select the code you want to modify, right-click to open the context menu, and look for the commands under **Shell Refactor**, or use the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and type `Bash Options` or `Shell Refactor`.
71 changes: 68 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,74 @@
"commands": [
{
"command": "bash.extractVariable",
"title": "Shell Refactor: Export Variable"
}
]
"title": "Shell Refactor: Extract Variable"
},
{
"command": "bash.extractFunction",
"title": "Shell Refactor: Extract Function"
},
{
"command": "bash.makeExecutable",
"title": "Shell Refactor: Make Executable"
},
{
"command": "bash.insertShebang",
"title": "Shell Refactor: Insert Shebang"
},
{
"command": "bash.toggleQuotes",
"title": "Shell Refactor: Toggle Quotes"
},
{
"command": "bash.toggleParameterExpansion",
"title": "Shell Refactor: Toggle Parameter Expansion"
},
{
"command": "bash.wrapInIf",
"title": "Shell Refactor: Wrap in If"
},
{
"command": "bash.wrapInFor",
"title": "Shell Refactor: Wrap in For Loop"
},
{
"command": "bash.convertToHereDoc",
"title": "Shell Refactor: Convert to HereDoc"
},
{
"command": "bash.debugPrint",
"title": "Shell Refactor: Debug Print"
}
],
"menus": {
"editor/context": [
{
"command": "bash.extractVariable",
"when": "editorHasSelection && resourceLangId == shellscript",
"group": "1_modification"
},
{
"command": "bash.extractFunction",
"when": "editorHasSelection && resourceLangId == shellscript",
"group": "1_modification"
},
{
"command": "bash.wrapInIf",
"when": "editorHasSelection && resourceLangId == shellscript",
"group": "1_modification"
},
{
"command": "bash.wrapInFor",
"when": "editorHasSelection && resourceLangId == shellscript",
"group": "1_modification"
},
{
"command": "bash.debugPrint",
"when": "editorHasSelection && resourceLangId == shellscript",
"group": "1_modification"
}
]
}
},
"scripts": {
"vscode:prepublish": "yarn run compile",
Expand Down
31 changes: 31 additions & 0 deletions src/commands/convertToHereDoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as vscode from 'vscode';

export async function convertToHereDoc() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const selection = editor.selection;
if (selection.isEmpty) {
return;
}

const text = editor.document.getText(selection);

// Check if text is an echo statement
// Simple heuristic: starts with echo
// This is hard to do robustly without parsing.
// Let's assume user selected the content they want IN the heredoc.

// Pattern: cat <<EOF
// text
// EOF

const snippet = new vscode.SnippetString(
'cat <<EOF\n' +
text + '\n' +
'EOF'
);

editor.insertSnippet(snippet, selection);
}
44 changes: 44 additions & 0 deletions src/commands/debugPrint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as vscode from 'vscode';

export async function debugPrint() {
const editor = vscode.window.activeTextEditor;
if (!editor) {
return;
}
const selection = editor.selection;
let text = "";
if (selection.isEmpty) {
// use word under cursor
const range = editor.document.getWordRangeAtPosition(selection.active);
if (range) {
text = editor.document.getText(range);
}
} else {
text = editor.document.getText(selection);
}

if (!text) {
return;
}

// Insert echo below current line
const line = editor.document.lineAt(selection.end.line);
const indent = line.text.substring(0, line.firstNonWhitespaceCharacterIndex); // preserve indentation

// Sanitize text for echo?
// If it looks like a variable (starts with $), print value.
// If it is just a word, assume it is a variable name and print "$VAR: $VAR"

let echoStmt = "";
if (text.startsWith('$')) {
echoStmt = `echo "DEBUG: ${text} = ${text}"`;
} else {
// Assume variable name
echoStmt = `echo "DEBUG: ${text} = $${text}"`;
}

const insertPos = new vscode.Position(line.lineNumber + 1, 0);
editor.edit(editBuilder => {
editBuilder.insert(insertPos, `${indent}${echoStmt}\n`);
});
}
53 changes: 53 additions & 0 deletions src/commands/extractFunction.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
85 changes: 85 additions & 0 deletions src/commands/extractVariable.ts
Original file line number Diff line number Diff line change
@@ -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}`);
});
}
20 changes: 20 additions & 0 deletions src/commands/insertShebang.ts
Original file line number Diff line number Diff line change
@@ -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');
});
}
22 changes: 22 additions & 0 deletions src/commands/makeExecutable.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
Loading