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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Each rule mirrors a well-known ShellCheck check.
| [`no-backticks`](./docs/rules/no-backticks.md) | SC2006 | Use `$(...)` instead of legacy backticks | ✅ | error |
| [`no-expansions-in-single-quotes`](./docs/rules/no-expansions-in-single-quotes.md) | SC2016 | Expressions don't expand in single quotes | | warn |
| [`no-unquoted-expansions`](./docs/rules/no-unquoted-expansions.md) | SC2086/46 | Quote expansions subject to word splitting and globbing | ✅ | error |
| [`no-useless-echo`](./docs/rules/no-useless-echo.md) | SC2116 | Disallow `$(echo ...)` | | error |

## Configuration comments

Expand Down
53 changes: 53 additions & 0 deletions docs/rules/no-useless-echo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# no-useless-echo

Disallow useless `echo` inside command substitutions.

## Background

A command substitution that only runs `echo`, such as `$(echo "$name")`, produces the same text that was passed to `echo`. It starts an extra process (or at least an extra step) and makes the code harder to read. Using the value directly is simpler.

## Rule Details

This rule warns about a command substitution, written as `$(...)` or with backticks, whose entire body is a single `echo` command without redirects.

The rule doesn't inspect `echo`'s options, so `$(echo -n "$value")` is also reported.

Substitutions that do more than echo, such as pipelines (`$(echo "$name" | tr a-z A-Z)`) or multiple commands, are not reported.

Examples of **incorrect** code for this rule:

```bash
# eslint bash/no-useless-echo: "error"

name=$(echo "$first")

ls $(echo /tmp)

files=`echo "$dir"`
```

Examples of **correct** code for this rule:

```bash
# eslint bash/no-useless-echo: "error"

name="$first"

upper=$(echo "$name" | tr a-z A-Z)

both=$(echo hello; echo world)

echo "hello"
```

## Options

This rule has no options.

## When Not to Use It

`$(echo ...)` is occasionally used on purpose for side effects of `echo`, such as collapsing whitespace in an unquoted value or interpreting escape sequences with `echo -e`. If you rely on those behaviors, use a disable comment for those lines or disable this rule.

## Prior Art

- [SC2116](https://www.shellcheck.net/wiki/SC2116)
1 change: 1 addition & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe("plugin", () => {
"no-backticks",
"no-expansions-in-single-quotes",
"no-unquoted-expansions",
"no-useless-echo",
]);
});

Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { BashLanguage } from "./languages/bash-language.js";
import noBackticks from "./rules/no-backticks.js";
import noExpansionsInSingleQuotes from "./rules/no-expansions-in-single-quotes.js";
import noUnquotedExpansions from "./rules/no-unquoted-expansions.js";
import noUselessEcho from "./rules/no-useless-echo.js";

const rules = {
"no-backticks": noBackticks,
"no-expansions-in-single-quotes": noExpansionsInSingleQuotes,
"no-unquoted-expansions": noUnquotedExpansions,
"no-useless-echo": noUselessEcho,
};

const plugin = {
Expand All @@ -34,6 +36,7 @@ const plugin = {
"bash/no-backticks": "error",
"bash/no-expansions-in-single-quotes": "warn",
"bash/no-unquoted-expansions": "error",
"bash/no-useless-echo": "error",
},
},
},
Expand Down
50 changes: 50 additions & 0 deletions src/rules/no-useless-echo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @fileoverview Tests for the no-useless-echo rule.
*/

import { RuleTester } from "eslint";
import { BashLanguage } from "../languages/bash-language.js";
import rule from "./no-useless-echo.js";

const ruleTester = new RuleTester({
plugins: {
bash: {
meta: { namespace: "shell" },
languages: { bash: new BashLanguage() },
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin shape is validated by ESLint at runtime.
} as any,
language: "bash/bash",
});

ruleTester.run("no-useless-echo", rule as never, {
valid: [
"echo hello",
"x=$(pwd)",
"x=$(echo foo | tr a-z A-Z)",
"x=$(echo foo; echo bar)",
"x=$(echo foo > file)",
"x=$(! echo foo)",
],
Comment thread
Copilot marked this conversation as resolved.
invalid: [
{
code: "x=$(echo foo)",
errors: [
{
messageId: "uselessEcho",
line: 1,
column: 3,
endColumn: 14,
},
],
},
{
code: 'cmd "$(echo "$var")"',
errors: [{ messageId: "uselessEcho" }],
},
{
code: "x=`echo foo`",
errors: [{ messageId: "uselessEcho" }],
},
],
});
48 changes: 48 additions & 0 deletions src/rules/no-useless-echo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @fileoverview Rule to disallow `$(echo ...)` command substitutions.
* Mirrors ShellCheck SC2116.
*/

import { isCommandNamed } from "./utils.js";
import type { BashRuleDefinition } from "../types.js";

const rule: BashRuleDefinition<{ MessageIds: "uselessEcho" }> = {
meta: {
type: "suggestion",
languages: ["shell/bash"],
docs: {
description: "Disallow useless `echo` inside command substitutions",
recommended: true,
dialects: ["Bash", "POSIX sh", "mksh"],
url: "https://github.com/eslint/bash/blob/main/docs/rules/no-useless-echo.md",
},
schema: [],
messages: {
uselessEcho:
"Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. (ShellCheck SC2116)",
},
},

create(context) {
return {
CommandSubstitution(node) {
const first = node.body[0];

if (
node.body.length === 1 &&
first !== undefined &&
isCommandNamed(first, "echo") &&
!first.negated &&
first.redirects.length === 0
) {
context.report({
node,
messageId: "uselessEcho",
});
}
},
};
},
};

export default rule;
Loading