A toolkit for bash script transformations, built with TypeScript on top of bash-traverse to provide a jscodeshift-style API for bash script transformations.
IMPORTANT Status: Experimental. Designed for real-world bash transformation, but not yet stable enough to treat as a drop-in engine for production refactors.
- jscodeshift-like API: Familiar API for writing transforms, similar to jscodeshift
- Bash AST Parsing: Uses @isdk/bash-parser for accurate parsing of bash scripts
- TypeScript Support: Full TypeScript support with type safety
- Node Constructors: Create AST nodes programmatically (like
j.ClassDeclaration) - Collection Methods: Find, filter, and transform nodes (like
ast.find()) - Node Manipulation: Replace, insert, and remove nodes
- CLI Tool: Command-line interface for running transforms
- Dry Run Mode: Preview changes without modifying files
- File Pattern Support: Use glob patterns to target multiple files
npm install -g bashcodeshiftOr install locally:
npm install bashcodeshiftbashcodeshift -t transform.js script.shbashcodeshift -t transform.js "**/*.sh"bashcodeshift -t transform.js "**/*.sh" --dryTransforms are JavaScript/TypeScript functions that receive file information and return modified source code, just like jscodeshift transforms.
import { TransformFunction } from 'bashcodeshift';
const transform: TransformFunction = (fileInfo, api, options) => {
const { source } = fileInfo;
const { b } = api;
// Parse source into AST (like jscodeshift's 'j' API)
const ast = b(source);
// Find and transform commands (like ast.find(j.ClassDeclaration))
ast.find('Command', { name: 'npm' })
.forEach(path => {
path.value.name = 'yarn';
});
// Return transformed source
return ast.toSource();
};
export default transform;The api object provides the following methods:
Parse bash source code into an AST and return a jscodeshift-like API, similar to jscodeshift's j() function.
ast.Command(props)- Shell commands:npm install,git checkoutast.Variable(props)- Variable assignments:VAR=valueast.Conditional(props)- If/else statementsast.Loop(props)- For/while loopsast.Function(props)- Function definitionsast.Pipeline(props)- Command pipelines:cmd1 | cmd2ast.Redirect(props)- I/O redirection:>,<,>>ast.Subshell(props)- Subshells:$(command)ast.Comment(props)- Comments:# comment
ast.find(nodeType, filter)- Find nodes matching criteriaast.filter(collection, predicate)- Filter a collectionast.forEach(collection, callback)- Iterate over a collectionast.map(collection, callback)- Transform a collectionast.size(collection)- Get collection size
path.replace(newNode)- Replace a nodepath.insertBefore(newNode)- Insert before a nodepath.insertAfter(newNode)- Insert after a nodepath.remove()- Remove a nodepath.prune()- Remove node and clean up
ast.toSource(options)- Generate source code from AST
import { TransformFunction } from 'bashcodeshift';
const transform: TransformFunction = (fileInfo, api, options) => {
const { source } = fileInfo;
const { b } = api;
const ast = b(source);
// Find all npm commands and convert to yarn
ast.find('Command', { name: 'npm' }).forEach(path => {
const command = path.value as any;
command.name = 'yarn';
if (command.arguments[0] === 'install') {
command.arguments = command.arguments.slice(1);
} else if (command.arguments[0] === 'uninstall') {
command.arguments = ['remove', ...command.arguments.slice(1)];
}
});
return ast.toSource();
};
export default transform;import { TransformFunction } from 'bashcodeshift';
const transform: TransformFunction = (fileInfo, api, options) => {
const { source } = fileInfo;
const { b } = api;
const ast = b(source);
// 1. Convert npm to yarn
ast.find('Command', { name: 'npm' }).forEach(path => {
const command = path.value as any;
command.name = 'yarn';
});
// 2. Add error handling to functions
ast.find('Function').forEach(path => {
const func = path.value as any;
const body = func.body;
// Check if set -e is already present
const hasSetE = body.some((stmt: any) =>
stmt.type === 'Command' && stmt.name === 'set' && stmt.arguments.includes('-e')
);
if (!hasSetE) {
// Add set -e at the beginning using node constructor
const setE = ast.Command({ name: 'set', arguments: ['-e'] });
body.unshift(setE);
}
});
// 3. Update git commands to modern syntax
ast.find('Command', { name: 'git' }).forEach(path => {
const command = path.value as any;
const args = command.arguments;
if (args[0] === 'checkout' && args[1] === '-b') {
// git checkout -b <branch> -> git switch -c <branch>
command.arguments = ['switch', '-c', ...args.slice(2)];
}
});
// 4. Add logging to important commands
const importantCommands = ['docker', 'kubectl', 'terraform'];
importantCommands.forEach(cmdName => {
ast.find('Command', { name: cmdName }).forEach(path => {
const command = path.value as any;
// Create a logging command using node constructor
const logCmd = ast.Command({
name: 'echo',
arguments: [`[INFO] Running: ${cmdName} ${command.arguments.join(' ')}`]
});
// Insert logging before the command
path.insertBefore(logCmd);
});
});
return ast.toSource();
};
export default transform;/**
* Transforms bash scripts for modern best practices.
*
* - Converts npm commands to yarn
* - Adds error handling to functions and loops
* - Updates git commands to modern syntax
* - Adds logging to important commands
*
* @param {any} fileInfo - File information
* @param {any} api - bashcodeshift API
*/
function transformBashScript(fileInfo, api) {
const { source } = fileInfo;
const { b } = api;
const ast = b(source);
// Convert npm to yarn (like your jscodeshift example)
ast.find('Command', { name: 'npm' }).forEach(path => {
const command = path.value;
command.name = 'yarn';
// Handle specific npm commands
if (command.arguments[0] === 'install') {
command.arguments = command.arguments.slice(1);
} else if (command.arguments[0] === 'uninstall') {
command.arguments = ['remove', ...command.arguments.slice(1)];
}
});
// Add error handling to functions (like your constructor example)
ast.find('Function').forEach(path => {
const func = path.value;
const body = func.body;
// Check if error handling is present
const hasErrorHandling = body.some(stmt =>
stmt.type === 'Command' && stmt.name === 'set' && stmt.arguments.includes('-e')
);
if (!hasErrorHandling) {
// Add error handling as the first statement
const setE = ast.Command({ name: 'set', arguments: ['-e'] });
body.unshift(setE);
}
});
// Update git commands (like your method modification example)
ast.find('Command', { name: 'git' }).forEach(path => {
const command = path.value;
const args = command.arguments;
if (args[0] === 'checkout') {
if (args[1] === '-b') {
// git checkout -b <branch> -> git switch -c <branch>
command.arguments = ['switch', '-c', ...args.slice(2)];
} else if (args.length > 1 && !args[1].startsWith('-')) {
// git checkout <branch> -> git switch <branch>
command.arguments = ['switch', ...args.slice(1)];
}
}
});
return ast.toSource();
}
export default transformBashScript;bashcodeshift [options] <path>
Options:
-t, --transform <path> Path to transform file
--parser <parser> Parser to use (default: bash)
--dry Dry run (no changes)
--print Print output for comparison
--verbose Show more information
--ignore-pattern <pattern> Ignore files matching pattern
-h, --help Display helpUse the provided test utilities to write tests for your transforms:
import { defineTest } from 'bashcodeshift/dist/test-utils';
defineTest(__dirname, 'update-package-manager', null, 'update-package-manager', { parser: 'bash' });- Node.js >= 16.0.0
- TypeScript >= 5.0.0
- Clone the repository
- Install dependencies:
npm install - Build the project:
npm run build - Run tests:
npm test
npm run build: Build TypeScript to JavaScriptnpm run dev: Watch mode for developmentnpm test: Run testsnpm run lint: Run ESLintnpm run docs: Generate documentation
- Type Safety: Catch errors at compile time
- Better IDE Support: IntelliSense and autocomplete
- Refactoring: Safe refactoring with confidence
- Documentation: Self-documenting code with types
- Maintainability: Easier to maintain and extend
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
MIT