Powershell: #!/usr/local/bin/powershell bang should not load profile

Created on 18 May 2016  路  35Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

Put in your PS profile

Write-Host 'PROFILE'

Create a script ./foo.ps1 and chmod +x on it

#!/usr/local/bin/powershell
Write-Host 'foo'

From bash call ./foo.ps1

Expected behavior

Should not load profile

ash-3.2$ ./foo.ps1
foo

Reason: that's how bash handles it

Actual behavior

Load profile

ash-3.2$ ./foo.ps1
PROFILE
foo

Environment data

v0.4.0

Committee-Reviewed Issue-Enhancement OS-Linux Usability WG-Engine

Most helpful comment

Note: In this discussion, we're equating PowerShell _profiles_ with _one_ type of initialization file in POSIX-like shells, the "_individual per-interactive-shell startup file"_, as Bash calls it, stored in ~/.*rc files; e.g., ~/.bashrc for Bash. In POSIX-like shells, _profiles_ are distinct initialization files (which, unlike the ~/.*rc files, exist both on a system and at a user level) only sourced by _login shells_, irrespective of interactivity; A POSIX-like shell is a login shell only if the -l option is explicitly passed or invocation happens via the login utility - PowerShell has _no_ equivalent to that, but in contrast with POSIX-like ~/.*rc files, PowerShell's profiles exist both at the current-user and all-users (and host) levels.

  • Shebang lines (#!...) on Unix platforms only every work with _absolute paths_.

  • Only _2_ tokens are portably supported (#!/path/to/foo bar; macOS supports more, but Linux doesn't).

  • Given that the powershell binary (a symlink to it) is installed in different locations across supported Unix platforms (/usr/bin on Linux vs. /usr/local/bin on macOS), the only way to portably target PowerShell is to invoke it via env, relying on env's ability to locate it via the $PATH, i.e.: #!/usr/bin/env powershell

    • This means that if you want your script to remain portable, passing any parameters to powershell itself as part of the shebang line is NOT an option, given that #!/usr/bin/env powershell already uses up the 2 tokens that are portably supported.

  • POSIX-like shells (bash, ksh, zsh) do NOT load their initialization files (~/.*rc) if _they themselves_ determine that they're being invoked _non-interactively_, such as via a shebang line.

    • Providing something like $host.IsInteractive would be helpful in general, but should _not_ come into play here; in fact, initialization files doing their own interactivity detection causes nothing but trouble, because it gets in the way of non-interactive scripts purposely re-sourcing initialization files after their modification.

    • Thus, if PowerShell wants to fit into the Unix world specifically and more generally wants to _provide a predictable scripting environment_ it _mustn't load profiles by default_ when executing scripts; unfortunately, that is a breaking change; see #3743

All 35 comments

Ha! I don't see anyway this is going to work without a shim that executes with -noprofile.

We just need to figure out who called us, you know.

Also, if you run the same way

#!powershell

Write-Host 'foo'
bash-3.2$ /Users/vors/dev/foo.ps1
Failed to load , error: dlopen(, 1): no suitable image found.  Did find:
    /usr/local/lib/: not a file
    /usr/lib/: not a file

I don't see how we would reliably determine who called us, and how to interpret it (I as a user might call PowerShell via env, and wouldn't want its behavior to change because it detected that env launched it instead of Bash, that's just a bad idea).

I think really we just want to run with -noprofile whenever an in-place script is run (and -noexit was _not_ used). Said another way, a profile should only be loaded when PowerShell is launched into an interactive shell.

This would be much better behavior (but we'd need a way to specify -profile to reverse it).

@lzybkr can you comment on the profile/noprofile, please ?

Should this work at all #!powershell ?

I just noticed this in my .bashrc:

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

So I would say we should run the profile with #!powershell, but we should also find a clean way of detecting the process is interactive, e.g. if ($host.IsInteractive)

:+1:
I was asking for $host.IsInteractive for ages.

Since #3558 merged it seems we can get a fix for this.
/cc @SteveL-MSFT @mklement0

Note: In this discussion, we're equating PowerShell _profiles_ with _one_ type of initialization file in POSIX-like shells, the "_individual per-interactive-shell startup file"_, as Bash calls it, stored in ~/.*rc files; e.g., ~/.bashrc for Bash. In POSIX-like shells, _profiles_ are distinct initialization files (which, unlike the ~/.*rc files, exist both on a system and at a user level) only sourced by _login shells_, irrespective of interactivity; A POSIX-like shell is a login shell only if the -l option is explicitly passed or invocation happens via the login utility - PowerShell has _no_ equivalent to that, but in contrast with POSIX-like ~/.*rc files, PowerShell's profiles exist both at the current-user and all-users (and host) levels.

  • Shebang lines (#!...) on Unix platforms only every work with _absolute paths_.

  • Only _2_ tokens are portably supported (#!/path/to/foo bar; macOS supports more, but Linux doesn't).

  • Given that the powershell binary (a symlink to it) is installed in different locations across supported Unix platforms (/usr/bin on Linux vs. /usr/local/bin on macOS), the only way to portably target PowerShell is to invoke it via env, relying on env's ability to locate it via the $PATH, i.e.: #!/usr/bin/env powershell

    • This means that if you want your script to remain portable, passing any parameters to powershell itself as part of the shebang line is NOT an option, given that #!/usr/bin/env powershell already uses up the 2 tokens that are portably supported.

  • POSIX-like shells (bash, ksh, zsh) do NOT load their initialization files (~/.*rc) if _they themselves_ determine that they're being invoked _non-interactively_, such as via a shebang line.

    • Providing something like $host.IsInteractive would be helpful in general, but should _not_ come into play here; in fact, initialization files doing their own interactivity detection causes nothing but trouble, because it gets in the way of non-interactive scripts purposely re-sourcing initialization files after their modification.

    • Thus, if PowerShell wants to fit into the Unix world specifically and more generally wants to _provide a predictable scripting environment_ it _mustn't load profiles by default_ when executing scripts; unfortunately, that is a breaking change; see #3743

Here's how Bash determines if an instance is non-interactive - such as when invoked via a shebang line (as stated, non-interactive instances by default do not load _any_ initialization files):

  An interactive shell is one started without non-option arguments 
  [read: a script filename operand optionally followed by its arguments] and
  without the -c option whose standard input and error are both connected
  to terminals (as determined by isatty(3)), or one started with  the  -i
  option.  PS1 is set and $- includes i if bash is interactive, allowing
  a shell script or a startup file to test this state.

Note that the use of shebang line #!/bin/bash in, for example, script /tmp/foo results in invocation
/bin/bash /tmp/foo.

Interesting, if we install PowerShell in different locations across supported Unix platforms why not create a link to current version in /bin? Have we bash on all platforms there?
Sometimes loading a profile in non-interactive mode can be useful, ex. loading a compatibility module. So automatic detection of non-interactive mode is not best way for PowerShell. Since we discuss using a short name (posh) we can resolve the problem with shebang by adding new wrappers:

  • #!/usr/bin/env posh-no-user-profile
  • #!/usr/bin/env posh-no-all-profile
  • #!/usr/bin/env posh-no-profiles

why not create a link to current version in /bin

At least Linux platforms conform to the FHS (Filesystem Hierarchy Standard), according to which placing a symlink to the PowerShell binary in /bin would be inappropriate; similarly, it would be unexpected on macOS.

Do we have bash on all platforms there?

Probably yes, but how does that come into play here?

Sometimes loading a profile in non-interactive mode can be useful

You can always do that from _within_ a script, if needed; e.g., . $PROFILE.

we can resolve the problem with shebang by adding new wrappers:

That seems like an obscure and cumbersome workaround.

That seems like an obscure and cumbersome workaround

If we want to be bash-behaivor-compatible we should use bash startup code and embed it in PowerShell startup code. But it doesn't seem compatible with PowerShell behavior. Dead lock :-). I think we're doomed to be rude and break a wall somewhere. Until we have the right solution, I prefer to have a wrapper. When it becomes stable, we could gradually embed it in the main code.

Not clear here if you're thinking of not automatically loading profiles just in the shebang case or making that the default. if just the shebang case you have the issue of a different behaviour. if you're talking about making the default not to load profiles on all platforms then that's a huge breaking change that will upset the majority of Windows users. This is a case of which option is going to upset the least number of users.

@RichardSiddaway Windows and Unix sometimes is very different. Windows PowerShell and Bash too. We need easy ways to adapt PowerShell to Unix users. So we need support common Unix scenarios without breaking Windows users. This often affects many _interrelated_ features.

FWIW we've already run into problems with shebang scripts spewing errors from users' profiles. I guess if my team doesn't care about macOS support we could use:

#!/usr/bin/powershell -noprofile

Too bad we can't do this in a way that would work on macOS as well.

@RichardSiddaway:

Note that a shebang line is just syntactic sugar (handled by the system) for invoking the executable named in it with the full path of containing file, so that #!/usr/bin/pwsh inside file /path/to/script is equivalent to explicit invocation /usr/bin/pwsh /path/to/script. Not only would it be difficult to distinguish these two forms, they _should_ be considered the same.

To be clear: the suggestion is not to make non-loading of the profile the default _categorically_, but only for _non-interactive_ scenarios, which notably includes executing a _script_.

This distinction is sensible:

  • When executing a _script_, do not load profiles, so as to provide a predictable, standardized execution environment.

  • When starting an _interactive_ session, load the profiles for a customized interactive experience with custom aliases, functions, ...

@rkeithhill's example shows that automatic loading of profiles for script execution is problematic in practice - ranging from distracting extraneous output / benign errors to fundamentally changing the script's behavior.

Also note that even if this issue is addressed, invoking *.ps1 files _in-process_ from an interactive session will continue to run in an environment customized by profiles.
(This issue doesn't even arise in POSIX-like shells, where all scripts invariably run in a _child_ process (unless explicitly _sourced_) that is unaffected by the parent shell's state, save for modifications to _environment variables_.)

As @iSazonov notes, something's gotta give.
If a breaking change is not an option: To modify his suggestion, perhaps a _single_ wrapper for pwsh -noprofile for use in shebang lines (or directly) is a reasonable compromise: #!/usr/bin/env pwsh-np

When executing a script, do not load profiles, so as to provide a predictable, standardized execution environment.

Agree 100%

@lzybkr Could you please comment? I remember you have some thoughts about profile loading.

One of possible solutions to that problem could be creation of system wide alias i.e. posh which would enforce Unix like behavior for shell scripts. This would make possible to have best of two worlds Windows convention based powershell (to preserve compatibility) and Unix convention based posh commands. For situations where current Windows powershell default behavior hurts see https://github.com/dotnet/source-build/issues/225 and issues linked there.

@4creators: Note that an _alias_ is not a viable solution, because the mechanism must be shell-independent.

Therefore, an _executable in the filesystem_ is needed that ensures the no-profile behavior.

In the simplest case, a mere _symlink_ such as /usr/bin/pwsh-np would suffice:
Assuming that PowerShell can determine the filename of a symlink that invoked it (which native programs can do), it can adjust the behavior accordingly.

The - suboptimal - fallback solution would be a wrapper script.

As a user, I might want the profile loaded in such scenarios so I can stop my admin from messing with my machine. 馃樃 馃檶

But more seriously - admins don't want the profile loaded in non-interactive invocations of PowerShell. They should always specify -noprofile, but they don't, even when they are aware of the potential issues.

@PowerShell/powershell-committee to review proposal to have something like pwsh-np

I'd prefer to see a _common conception_ for interactive vs non-interactive vs login.
We could have some startup wrappers like pwsh-np.
Also we have a request to have a lightweight client for remoting.

@PowerShell/powershell-committee reviewed this and is ok with the pwsh-np proposal. Recommend investigating using a symlink/hardlink to avoid having multiple copies of the host binary.

@SteveL-MSFT: Note that on Windows neither a symlink nor a hardlink works - see https://github.com/dotnet/core-setup/issues/3260

Could we implement pwsh-np as a custom pwsh.dll loader?

@iSazonov: I haven't taken a closer look, but it seems that .NET Core 2.1 may offer a solution.
From https://github.com/dotnet/core-setup/issues/3260#issuecomment-375780087:

https://github.com/dotnet/core-setup/issues/3720 will provide a way to have a renamed exe (apphost) plus point to another location that contains the application files. However it does require a local startupconfig.json file to tell it where the app files are located. So the exe and a simple json file are similar in functionality to the symbolic link original requirements and this new functionality will be leveraged by global tools.

If I understand this correctly, then a shim executable with accompanying startupconfig.json will do.

As an aside: As I've mentioned in the linked issue, I can't help but think that a symlink-based solution should be technically possible, via GetFinalPathNameByHandle, but from the absence of feedback I infer that there's no intent to tackle this.

@mklement0 3720 was closed and they don't track closed issues - so you should open new issue for GetFinalPathNameByHandle.

As for the solution I hope we'll able test this on next week after moving to .Net Core 2.1.

@iSazonov: Good idea - please see https://github.com/dotnet/core-setup/issues/4080

I'm really not a fan of using a different binary - I think far too many people will use pwsh because of familiarity and that will be fine on their machine and maybe in production, but will fail on some random dev box.

We can look at it from the other side based on #3743 and introduce one exe as POSIX-like (or even POSIX-compliant) and second one with traditional behavior.
For example powershell.exe - traditional behavior, pwsh.exe - POSIX-like.

It would really be nice to have something for 6.1 that works on Windows as well. I've been trying to use git pre-commit hooks on my Windows dev box to ensure I have the right email configured. This works but it slows down all my commits because it processes my profile scripts when it does not need to:

#!/usr/bin/env pwsh

# Verify user's Git config has appropriate email address
if ($env:GIT_AUTHOR_EMAIL -notmatch '@(non\.)?acme\.com$') {
    Write-Warning "Your Git email address '$env:GIT_AUTHOR_EMAIL' is not configured correctly."
    Write-Warning "It should end with '@acme.com' or '@non.acme.com'."
    Write-Warning "Use the command: 'git config --global user.email <[email protected]>' to set it correctly."
    exit 1
}

exit 0

The only work around I've found is to put the PS script in a separate pre-commit.ps1 file and modify pre-commit to:

#!/bin/sh
pwsh.exe -NoProfile -File .git/hooks/pre-commit.ps1

But I'd rather there be just a single file. At this point, I'd be satisfied if I could do this:

#!/usr/bin/env pwsh-np
...

@rkeithhill:

It's obviously just a stopgap, but you can roll your own pwsh-np by creating a script by that name with the content below and by placing it in a folder in your PATH (works on both Windows and Unix-like platforms, but on the latter you must also mark the script as executable (chmod a+x pwsh-np)):

#!/bin/sh

pwsh -noprofile "$@"

Your hooks should then be able to use the shebang you mentioned (#!/usr/bin/env pwsh-np).

Able to Run .ps1 script on zsh manually, however if call same ps1 from ansible playbook gets error that Connect-AzAccount is not recognized. #!/usr/bin/env pwsh is what i use in .ps1
Please help me to setup my environment variable.

@nippyin Please open new issue with step-by-step repo.

Was this page helpful?
0 / 5 - 0 ratings