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 @@ -78,6 +78,7 @@ Each rule mirrors a well-known ShellCheck check.
| [`no-unquoted-expansions`](./docs/rules/no-unquoted-expansions.md) | SC2086/46 | Quote expansions subject to word splitting and globbing | ✅ | error |
| [`no-useless-cat`](./docs/rules/no-useless-cat.md) | SC2002 | Don't pipe from a single-file `cat` | | error |
| [`no-useless-echo`](./docs/rules/no-useless-echo.md) | SC2116 | Disallow `$(echo ...)` | | error |
| [`require-read-r`](./docs/rules/require-read-r.md) | SC2162 | Use `read -r` so backslashes aren't mangled | ✅ | error |

## Configuration comments

Expand Down
51 changes: 51 additions & 0 deletions docs/rules/require-read-r.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# require-read-r

Require `read -r` so backslashes are not mangled.

## Background

By default, `read` treats backslashes in its input as escape characters: it removes them and joins lines that end in a backslash. Input such as `C:\temp` or a line ending in `\` is silently changed. The `-r` option makes `read` keep backslashes as they are, which is almost always what you want.

## Rule Details

This rule warns about `read` commands that don't pass the `-r` option. The option can appear alone (`-r`) or combined with other short options (`-rs`), anywhere before `--`.

This rule is autofixable: it inserts `-r` immediately after `read`.

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

```bash
# eslint bash/require-read-r: "error"

read line

read -p "Name: " name

while read line; do echo "$line"; done < input.txt
```

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

```bash
# eslint bash/require-read-r: "error"

read -r line

read -r -p "Name: " name

read -rs password

while IFS= read -r line; do echo "$line"; done < input.txt
```

## Options

This rule has no options.

## When Not to Use It

If you intentionally want `read` to interpret backslash escapes, for example to allow line continuations in input, you can disable this rule for those lines.

## Prior Art

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

Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import noLsIteration from "./rules/no-ls-iteration.js";
import noUnquotedExpansions from "./rules/no-unquoted-expansions.js";
import noUselessCat from "./rules/no-useless-cat.js";
import noUselessEcho from "./rules/no-useless-echo.js";
import requireReadR from "./rules/require-read-r.js";

const rules = {
"no-backticks": noBackticks,
Expand All @@ -18,6 +19,7 @@ const rules = {
"no-unquoted-expansions": noUnquotedExpansions,
"no-useless-cat": noUselessCat,
"no-useless-echo": noUselessEcho,
"require-read-r": requireReadR,
};

const plugin = {
Expand All @@ -43,6 +45,7 @@ const plugin = {
"bash/no-unquoted-expansions": "error",
"bash/no-useless-cat": "error",
"bash/no-useless-echo": "error",
"bash/require-read-r": "error",
},
},
},
Expand Down
63 changes: 63 additions & 0 deletions src/rules/require-read-r.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @fileoverview Tests for the require-read-r rule.
*/

import { RuleTester } from "eslint";
import { BashLanguage } from "../languages/bash-language.js";
import rule from "./require-read-r.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("require-read-r", rule as never, {
valid: [
"read -r line",
"read -rs password",
"read -r -a parts",
'while read -r line; do echo "$line"; done < file',
// Not the read builtin
"gread line",
],
invalid: [
{
code: "read line",
output: "read -r line",
errors: [
{
messageId: "missingR",
line: 1,
column: 1,
endColumn: 5,
},
],
},
{
code: "read -s password",
output: "read -r -s password",
errors: [{ messageId: "missingR" }],
},
{
code: "read -d -r value",
output: "read -r -d -r value",
errors: [{ messageId: "missingR" }],
},
{
code: "read -dr value",
output: "read -r -dr value",
errors: [{ messageId: "missingR" }],
},
{
code: 'while read line; do echo "$line"; done < file',
output: 'while read -r line; do echo "$line"; done < file',
errors: [{ messageId: "missingR" }],
},
],
});
102 changes: 102 additions & 0 deletions src/rules/require-read-r.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @fileoverview Rule to require the `-r` flag when using `read`.
* Mirrors ShellCheck SC2162.
*/

import { getCommandName, getStaticText } from "./utils.js";
import type { BashRuleDefinition } from "../types.js";

const optionsWithArguments = new Set(["a", "d", "i", "n", "N", "p", "t", "u"]);

const rule: BashRuleDefinition<{ MessageIds: "missingR" }> = {
meta: {
type: "problem",
languages: ["shell/bash"],
docs: {
description: "Require `read -r` so backslashes are not mangled",
recommended: true,
dialects: ["Bash", "POSIX sh", "mksh"],
url: "https://github.com/eslint/bash/blob/main/docs/rules/require-read-r.md",
},
fixable: "code",
schema: [],
messages: {
missingR:
"read without -r will mangle backslashes. (ShellCheck SC2162)",
},
},

create(context) {
return {
Command(node) {
if (getCommandName(node) !== "read" || !node.name) {
return;
}

let hasRawOption = false;

for (let index = 0; index < node.arguments.length; index++) {
const argument = node.arguments[index];
if (!argument) {
continue;
}

const text = getStaticText(argument);
if (text === "--") {
break;
}

if (
text === null ||
!text.startsWith("-") ||
text === "-"
) {
continue;
}

for (
let optionIndex = 1;
optionIndex < text.length;
optionIndex++
) {
const option = text[optionIndex];
if (!option) {
break;
}

if (option === "r") {
hasRawOption = true;
break;
}

if (optionsWithArguments.has(option)) {
if (optionIndex === text.length - 1) {
index++;
}

break;
}
}

if (hasRawOption) {
break;
}
}

if (hasRawOption) {
return;
}

const name = node.name;

context.report({
node: name,
messageId: "missingR",
fix: fixer => fixer.insertTextAfter(name, " -r"),
});
},
};
},
};

export default rule;
8 changes: 8 additions & 0 deletions tests/autofix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,12 @@ describe("autofix", () => {

expect(result.output).toBe('cp "$src" "$dest"\n');
});

it("should add -r to read", () => {
const result = fix('read line\necho "$line"\n', {
"bash/require-read-r": "error",
});

expect(result.output).toBe('read -r line\necho "$line"\n');
});
});
Loading