Skip to content

Commit 8d28c71

Browse files
authored
fix #1280 handle env in shebang (DonJayamanne#1290)
* handle shebangs that resolve paths from env * oops * make test more specific * handle promise
1 parent 14ef606 commit 8d28c71

6 files changed

Lines changed: 117 additions & 60 deletions

File tree

src/client/providers/setInterpreterProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ export class SetInterpreterProvider implements vscode.Disposable {
6363
this.presentQuickPick();
6464
}
6565

66-
private setShebangInterpreter() {
67-
const shebang = ShebangCodeLensProvider.detectShebang(vscode.window.activeTextEditor.document);
66+
private async setShebangInterpreter() {
67+
const shebang = await ShebangCodeLensProvider.detectShebang(vscode.window.activeTextEditor.document);
6868
if (shebang) {
6969
this.interpreterManager.setPythonPath(shebang);
7070
}
Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,72 @@
11
"use strict";
2-
import * as vscode from 'vscode'
3-
import { TextDocument, CodeLens, CancellationToken } from 'vscode'
4-
2+
import { IS_WINDOWS } from '../common/utils';
3+
import * as vscode from 'vscode';
4+
import * as child_process from 'child_process';
5+
import * as settings from '../common/configSettings';
6+
import { TextDocument, CodeLens, CancellationToken } from 'vscode';
7+
import { getFirstNonEmptyLineFromMultilineString } from '../interpreter/helpers';
58
export class ShebangCodeLensProvider implements vscode.CodeLensProvider {
6-
private settings;
7-
8-
// reload codeLenses on every configuration change.
99
onDidChangeCodeLenses: vscode.Event<void> = vscode.workspace.onDidChangeConfiguration;
1010

11-
public provideCodeLenses(document: TextDocument, token: CancellationToken): Thenable<CodeLens[]> {
12-
this.settings = vscode.workspace.getConfiguration('python');
13-
const codeLenses = this.createShebangCodeLens(document);
14-
11+
public async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise<CodeLens[]> {
12+
const codeLenses = await this.createShebangCodeLens(document);
1513
return Promise.resolve(codeLenses);
1614
}
1715

18-
private createShebangCodeLens(document: TextDocument) {
19-
const shebang = ShebangCodeLensProvider.detectShebang(document)
20-
if (!shebang || shebang === this.settings.get('pythonPath')) {
21-
// no shebang detected or interpreter is already set to shebang
22-
return;
16+
private async createShebangCodeLens(document: TextDocument) {
17+
const shebang = await ShebangCodeLensProvider.detectShebang(document);
18+
if (!shebang || shebang === settings.PythonSettings.getInstance().pythonPath) {
19+
return [];
2320
}
2421

25-
// create CodeLens
2622
const firstLine = document.lineAt(0);
2723
const startOfShebang = new vscode.Position(0, 0);
2824
const endOfShebang = new vscode.Position(0, firstLine.text.length - 1);
2925
const shebangRange = new vscode.Range(startOfShebang, endOfShebang);
30-
31-
const cmd : vscode.Command = {
32-
command: 'python.setShebangInterpreter',
26+
27+
const cmd: vscode.Command = {
28+
command: 'python.setShebangInterpreter',
3329
title: 'Set as interpreter'
34-
}
30+
};
3531

3632
const codeLenses = [(new CodeLens(shebangRange, cmd))];
3733
return codeLenses;
3834
}
3935

40-
public static detectShebang(document: TextDocument) {
41-
let error = false;
42-
36+
public static async detectShebang(document: TextDocument): Promise<string | undefined> {
4337
let firstLine = document.lineAt(0);
4438
if (firstLine.isEmptyOrWhitespace) {
45-
error = true;
39+
return;
40+
}
41+
42+
if (!firstLine.text.startsWith('#!')) {
43+
return;
4644
}
47-
48-
if (!error && "#!" === firstLine.text.substr(0, 2)) {
49-
// Shebang detected
50-
const shebang = firstLine.text.substr(2).trim();
51-
return shebang;
45+
46+
const shebang = firstLine.text.substr(2).trim();
47+
const pythonPath = await ShebangCodeLensProvider.getFullyQualifiedPathToInterpreter(shebang);
48+
return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined;
49+
}
50+
private static async getFullyQualifiedPathToInterpreter(pythonPath: string) {
51+
if (pythonPath.indexOf('bin/env ') >= 0 && !IS_WINDOWS) {
52+
// In case we have pythonPath as '/usr/bin/env python'
53+
return new Promise<string>(resolve => {
54+
const command = child_process.exec(`${pythonPath} -c 'import sys;print(sys.executable)'`);
55+
let result = '';
56+
command.stdout.on('data', (data) => {
57+
result += data.toString();
58+
});
59+
command.on('close', () => {
60+
resolve(getFirstNonEmptyLineFromMultilineString(result));
61+
});
62+
});
63+
}
64+
else {
65+
return new Promise<string>(resolve => {
66+
child_process.execFile(pythonPath, ["-c", "import sys;print(sys.executable)"], (_, stdout) => {
67+
resolve(getFirstNonEmptyLineFromMultilineString(stdout));
68+
});
69+
});
5270
}
53-
54-
return null;
5571
}
56-
5772
}
Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,85 @@
11
import * as assert from 'assert';
22
import * as path from 'path';
33
import * as vscode from 'vscode';
4-
import { ShebangCodeLensProvider } from '../../client/providers/shebangCodeLensProvider'
4+
import * as child_process from 'child_process';
5+
import { IS_WINDOWS } from '../../client/common/configSettings';
6+
import { ShebangCodeLensProvider } from '../../client/providers/shebangCodeLensProvider';
57

68
import { initialize, IS_TRAVIS, closeActiveWindows } from '../initialize';
9+
import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers';
710

811
const autoCompPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'shebang');
912
const fileShebang = path.join(autoCompPath, 'shebang.py');
13+
const fileShebangEnv = path.join(autoCompPath, 'shebangEnv.py');
14+
const fileShebangInvalid = path.join(autoCompPath, 'shebangInvalid.py');
1015
const filePlain = path.join(autoCompPath, 'plain.py');
1116

12-
var settings = vscode.workspace.getConfiguration("python");
13-
const origPythonPath = settings.get("pythonPath");
17+
var settings = vscode.workspace.getConfiguration('python');
18+
const origPythonPath = settings.get('pythonPath');
1419

15-
suite("Shebang detection", () => {
16-
suiteSetup(async () => {
17-
await initialize();
20+
suite('Shebang detection', () => {
21+
suiteSetup(() => initialize());
22+
suiteTeardown(() => vscode.workspace.getConfiguration('python').update('pythonPath', origPythonPath));
23+
teardown(async () => {
24+
await closeActiveWindows();
25+
await vscode.workspace.getConfiguration('python').update('pythonPath', origPythonPath);
1826
});
1927

20-
suiteTeardown(async () => {
21-
await vscode.workspace.getConfiguration("python").update("pythonPath", origPythonPath);
22-
});
23-
24-
teardown(() => closeActiveWindows());
25-
setup(() => {
26-
settings = vscode.workspace.getConfiguration("python");
27-
});
28-
29-
test("Shebang available, CodeLens showing", async () => {
30-
await settings.update("pythonPath", "python");
28+
test('Shebang available, CodeLens showing', async () => {
29+
await settings.update('pythonPath', 'someUnknownInterpreter');
3130
const editor = await openFile(fileShebang);
3231
const codeLenses = await setupCodeLens(editor);
3332

34-
assert.equal(codeLenses.length, 1, "No CodeLens available");
33+
assert.equal(codeLenses.length, 1, 'No CodeLens available');
3534
let codeLens = codeLenses[0];
3635
assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range');
3736
assert.equal(codeLens.command.command, 'python.setShebangInterpreter');
3837

3938
});
4039

41-
test("Shebang available, CodeLens hiding", async () => {
42-
await settings.update("pythonPath", "/usr/bin/test");
40+
test('Shebang available, CodeLens hiding', async () => {
41+
const pythonPath = await getFullyQualifiedPathToInterpreter('python');
42+
await settings.update('pythonPath', pythonPath);
4343
const editor = await openFile(fileShebang);
4444
const codeLenses = await setupCodeLens(editor);
45-
assert(!codeLenses, "CodeLens available although interpreters are equal");
45+
assert.equal(codeLenses.length, 0, 'CodeLens available although interpreters are equal');
4646

4747
});
4848

49-
test("Shebang missing, CodeLens hiding", async () => {
50-
const editor = await openFile(filePlain);
49+
test('Shebang not available (invalid shebang)', async () => {
50+
const pythonPath = await getFullyQualifiedPathToInterpreter('python');
51+
await settings.update('pythonPath', pythonPath);
52+
const editor = await openFile(fileShebangInvalid);
5153
const codeLenses = await setupCodeLens(editor);
52-
assert(!codeLenses, "CodeLens available although no shebang");
54+
assert.equal(codeLenses.length, 0, 'CodeLens available although shebang is invalid');
55+
});
56+
57+
if (!IS_WINDOWS) {
58+
test('Shebang available, CodeLens showing with env', async () => {
59+
await settings.update('pythonPath', 'p1');
60+
const editor = await openFile(fileShebangEnv);
61+
const codeLenses = await setupCodeLens(editor);
5362

63+
assert.equal(codeLenses.length, 1, 'No CodeLens available');
64+
let codeLens = codeLenses[0];
65+
assert(codeLens.range.isSingleLine, 'Invalid CodeLens Range');
66+
assert.equal(codeLens.command.command, 'python.setShebangInterpreter');
67+
68+
});
69+
70+
test('Shebang available, CodeLens hiding with env', async () => {
71+
const pythonPath = await getFullyQualifiedPathToInterpreter('python');
72+
await settings.update('pythonPath', pythonPath);
73+
const editor = await openFile(fileShebangEnv);
74+
const codeLenses = await setupCodeLens(editor);
75+
assert.equal(codeLenses.length, 0, 'CodeLens available although interpreters are equal');
76+
});
77+
}
78+
79+
test('Shebang missing, CodeLens hiding', async () => {
80+
const editor = await openFile(filePlain);
81+
const codeLenses = await setupCodeLens(editor);
82+
assert.equal(codeLenses.length, 0, 'CodeLens available although no shebang');
5483
});
5584

5685
async function openFile(fileName: string) {
@@ -59,11 +88,18 @@ suite("Shebang detection", () => {
5988
assert(vscode.window.activeTextEditor, 'No active editor');
6089
return editor;
6190
}
91+
async function getFullyQualifiedPathToInterpreter(pythonPath: string) {
92+
return new Promise<string>(resolve => {
93+
child_process.execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_, stdout) => {
94+
resolve(getFirstNonEmptyLineFromMultilineString(stdout));
95+
});
96+
}).catch(() => undefined);
97+
}
6298

6399
async function setupCodeLens(editor: vscode.TextEditor) {
64100
const document = editor.document;
65101
const codeLensProvider = new ShebangCodeLensProvider();
66102
const codeLenses = await codeLensProvider.provideCodeLenses(document, null);
67103
return codeLenses;
68104
}
69-
});
105+
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
#!/usr/bin/test
1+
#!python
22

33
print("dummy")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env python
2+
3+
print("dummy")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env1234 python
2+
3+
print("dummy")

0 commit comments

Comments
 (0)