fix(shell): quote paths properly in init scripts

This commit is contained in:
L. Yeung 2022-08-08 10:35:53 +08:00 committed by Jan De Dobbeleer
parent 2ed2c038af
commit db8eac7c5d
8 changed files with 232 additions and 50 deletions

View file

@ -3,6 +3,7 @@ package shell
import (
_ "embed"
"path/filepath"
"runtime"
"strconv"
"fmt"
@ -51,25 +52,111 @@ func getExecutablePath(env environment.Environment) (string, error) {
// On Windows, it fails when the excutable is called in MSYS2 for example
// which uses unix style paths to resolve the executable's location.
// PowerShell knows how to resolve both, so we can swap this without any issue.
executable = strings.ReplaceAll(executable, "\\", "/")
switch env.Flags().Shell {
case BASH, ZSH:
executable = strings.ReplaceAll(executable, " ", "\\ ")
executable = strings.ReplaceAll(executable, "(", "\\(")
executable = strings.ReplaceAll(executable, ")", "\\)")
if runtime.GOOS == environment.WINDOWS {
executable = strings.ReplaceAll(executable, "\\", "/")
}
return executable, nil
}
func Init(env environment.Environment) string {
executable, err := getExecutablePath(env)
if err != nil {
return noExe
func quotePwshStr(str string) string {
return fmt.Sprintf("'%s'", strings.ReplaceAll(str, "'", "''"))
}
func quotePosixStr(str string) string {
if len(str) == 0 {
return "''"
}
needQuoting := false
var b strings.Builder
for _, r := range str {
normal := false
switch r {
case '!', ';', '"', '(', ')', '[', ']', '{', '}', '$', '|', '&', '>', '<', '`', ' ', '#', '~', '*', '?', '=':
b.WriteRune(r)
case '\\', '\'':
b.WriteByte('\\')
b.WriteRune(r)
case '\a':
b.WriteString(`\a`)
case '\b':
b.WriteString(`\b`)
case '\f':
b.WriteString(`\f`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
case '\v':
b.WriteString(`\v`)
default:
b.WriteRune(r)
normal = true
}
if !normal {
needQuoting = true
}
}
// the quoting form $'...' is used for a string includes any special characters
if needQuoting {
return fmt.Sprintf("$'%s'", b.String())
}
return b.String()
}
func quoteFishStr(str string) string {
if len(str) == 0 {
return "''"
}
needQuoting := false
var b strings.Builder
for _, r := range str {
normal := false
switch r {
case ';', '"', '(', ')', '[', ']', '{', '}', '$', '|', '&', '>', '<', ' ', '#', '~', '*', '?', '=':
b.WriteRune(r)
case '\\', '\'':
b.WriteByte('\\')
b.WriteRune(r)
default:
b.WriteRune(r)
normal = true
}
if !normal {
needQuoting = true
}
}
// single quotes are used when the string includes any special characters
if needQuoting {
return fmt.Sprintf("'%s'", b.String())
}
return b.String()
}
func quoteLuaStr(str string) string {
if len(str) == 0 {
return "''"
}
return fmt.Sprintf("'%s'", strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(str))
}
func quoteNuStr(str string) string {
if len(str) == 0 {
return "''"
}
return fmt.Sprintf(`"%s"`, strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(str))
}
func Init(env environment.Environment) string {
shell := env.Flags().Shell
switch shell {
case PWSH, PWSH5:
return fmt.Sprintf("(@(&\"%s\" init %s --config=\"%s\" --print) -join \"`n\") | Invoke-Expression", executable, shell, env.Flags().Config)
executable, err := getExecutablePath(env)
if err != nil {
return noExe
}
return fmt.Sprintf("(@(& %s init %s --config=%s --print) -join \"`n\") | Invoke-Expression", quotePwshStr(executable), shell, quotePwshStr(env.Flags().Config))
case ZSH, BASH, FISH, CMD:
return PrintInit(env)
case NU:
@ -87,32 +174,43 @@ func PrintInit(env environment.Environment) string {
}
shell := env.Flags().Shell
configFile := env.Flags().Config
var script string
switch shell {
case PWSH, PWSH5:
script := getShellInitScript(executable, configFile, pwshInit)
return strings.ReplaceAll(script, "::SHELL::", shell)
executable = quotePwshStr(executable)
configFile = quotePwshStr(configFile)
script = pwshInit
case ZSH:
return getShellInitScript(executable, configFile, zshInit)
executable = quotePosixStr(executable)
configFile = quotePosixStr(configFile)
script = zshInit
case BASH:
return getShellInitScript(executable, configFile, bashInit)
executable = quotePosixStr(executable)
configFile = quotePosixStr(configFile)
script = bashInit
case FISH:
return getShellInitScript(executable, configFile, fishInit)
executable = quoteFishStr(executable)
configFile = quoteFishStr(configFile)
script = fishInit
case CMD:
return getShellInitScript(executable, configFile, cmdInit)
executable = quoteLuaStr(executable)
configFile = quoteLuaStr(configFile)
script = cmdInit
case NU:
return getShellInitScript(executable, configFile, nuInit)
executable = quoteNuStr(executable)
configFile = quoteNuStr(configFile)
script = nuInit
default:
return fmt.Sprintf("echo \"No initialization script available for %s\"", shell)
}
}
func getShellInitScript(executable, configFile, script string) string {
script = strings.ReplaceAll(script, "::OMP::", executable)
script = strings.ReplaceAll(script, "::CONFIG::", configFile)
script = strings.ReplaceAll(script, "::TRANSIENT::", strconv.FormatBool(Transient))
script = strings.ReplaceAll(script, "::ERROR_LINE::", strconv.FormatBool(ErrorLine))
script = strings.ReplaceAll(script, "::TOOLTIPS::", strconv.FormatBool(Tooltips))
return script
return strings.NewReplacer(
"::OMP::", executable,
"::CONFIG::", configFile,
"::SHELL::", shell,
"::TRANSIENT::", strconv.FormatBool(Transient),
"::ERROR_LINE::", strconv.FormatBool(ErrorLine),
"::TOOLTIPS::", strconv.FormatBool(Tooltips),
).Replace(script)
}
func createNuInit(env environment.Environment) {

View file

@ -1,6 +1,7 @@
package shell
import (
"fmt"
"oh-my-posh/environment"
"oh-my-posh/mock"
"testing"
@ -29,3 +30,83 @@ func TestConsoleBackgroundColorTemplate(t *testing.T) {
assert.Equal(t, tc.Expected, color, tc.Case)
}
}
func TestQuotePwshStr(t *testing.T) {
tests := []struct {
str string
expected string
}{
{str: ``, expected: `''`},
{str: `/tmp/oh-my-posh`, expected: `'/tmp/oh-my-posh'`},
{str: `/tmp/omp's dir/oh-my-posh`, expected: `'/tmp/omp''s dir/oh-my-posh'`},
{str: `C:\tmp\oh-my-posh.exe`, expected: `'C:\tmp\oh-my-posh.exe'`},
{str: `C:\tmp\omp's dir\oh-my-posh.exe`, expected: `'C:\tmp\omp''s dir\oh-my-posh.exe'`},
}
for _, tc := range tests {
assert.Equal(t, tc.expected, quotePwshStr(tc.str), fmt.Sprintf("quotePwshStr: %s", tc.str))
}
}
func TestQuotePosixStr(t *testing.T) {
tests := []struct {
str string
expected string
}{
{str: ``, expected: `''`},
{str: `/tmp/oh-my-posh`, expected: `/tmp/oh-my-posh`},
{str: `/tmp/omp's dir/oh-my-posh`, expected: `$'/tmp/omp\'s dir/oh-my-posh'`},
{str: `C:/tmp/oh-my-posh.exe`, expected: `C:/tmp/oh-my-posh.exe`},
{str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `$'C:/tmp/omp\'s dir/oh-my-posh.exe'`},
}
for _, tc := range tests {
assert.Equal(t, tc.expected, quotePosixStr(tc.str), fmt.Sprintf("quotePosixStr: %s", tc.str))
}
}
func TestQuoteFishStr(t *testing.T) {
tests := []struct {
str string
expected string
}{
{str: ``, expected: `''`},
{str: `/tmp/oh-my-posh`, expected: `/tmp/oh-my-posh`},
{str: `/tmp/omp's dir/oh-my-posh`, expected: `'/tmp/omp\'s dir/oh-my-posh'`},
{str: `C:/tmp/oh-my-posh.exe`, expected: `C:/tmp/oh-my-posh.exe`},
{str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `'C:/tmp/omp\'s dir/oh-my-posh.exe'`},
}
for _, tc := range tests {
assert.Equal(t, tc.expected, quoteFishStr(tc.str), fmt.Sprintf("quoteFishStr: %s", tc.str))
}
}
func TestQuoteLuaStr(t *testing.T) {
tests := []struct {
str string
expected string
}{
{str: ``, expected: `''`},
{str: `/tmp/oh-my-posh`, expected: `'/tmp/oh-my-posh'`},
{str: `/tmp/omp's dir/oh-my-posh`, expected: `'/tmp/omp\'s dir/oh-my-posh'`},
{str: `C:/tmp/oh-my-posh.exe`, expected: `'C:/tmp/oh-my-posh.exe'`},
{str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `'C:/tmp/omp\'s dir/oh-my-posh.exe'`},
}
for _, tc := range tests {
assert.Equal(t, tc.expected, quoteLuaStr(tc.str), fmt.Sprintf("quoteLuaStr: %s", tc.str))
}
}
func TestQuoteNuStr(t *testing.T) {
tests := []struct {
str string
expected string
}{
{str: ``, expected: `''`},
{str: `/tmp/oh-my-posh`, expected: `"/tmp/oh-my-posh"`},
{str: `/tmp/omp's dir/oh-my-posh`, expected: `"/tmp/omp's dir/oh-my-posh"`},
{str: `C:/tmp/oh-my-posh.exe`, expected: `"C:/tmp/oh-my-posh.exe"`},
{str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `"C:/tmp/omp's dir/oh-my-posh.exe"`},
}
for _, tc := range tests {
assert.Equal(t, tc.expected, quoteNuStr(tc.str), fmt.Sprintf("quoteNuStr: %s", tc.str))
}
}

View file

@ -1,4 +1,4 @@
export POSH_THEME='::CONFIG::'
export POSH_THEME=::CONFIG::
export POWERLINE_COMMAND="oh-my-posh"
export CONDA_PROMPT_MODIFIER=false
@ -10,15 +10,18 @@ if [[ ! -d "/tmp" ]]; then
fi
# start timer on command start
PS0='$(::OMP:: get millis > "$TIMER_START")'
PS0='$(_omp_start_timer)'
# set secondary prompt
PS2="$(::OMP:: print secondary --config="$POSH_THEME" --shell=bash --shell-version="$BASH_VERSION")"
function _omp_start_timer() {
::OMP:: get millis > "$TIMER_START"
}
function _omp_hook() {
local ret=$?
omp_stack_count=$((${#DIRSTACK[@]} - 1))
omp_elapsed=-1
local omp_stack_count=$((${#DIRSTACK[@]} - 1))
local omp_elapsed=-1
if [[ -f "$TIMER_START" ]]; then
omp_now=$(::OMP:: get millis)
omp_start_time=$(cat "$TIMER_START")

View file

@ -1,4 +1,4 @@
set --export POSH_THEME '::CONFIG::'
set --export POSH_THEME ::CONFIG::
set --global POWERLINE_COMMAND "oh-my-posh"
set --global CONDA_PROMPT_MODIFIER false
set --global omp_tooltip_command ""
@ -11,7 +11,7 @@ function fish_prompt
# see https://github.com/fish-shell/fish-shell/issues/8418
printf \e\[0J
if test "$omp_transient" = "1"
'::OMP::' print transient --config $POSH_THEME --shell fish --error $omp_status_cache --execution-time $omp_duration --stack-count $omp_stack_count --shell-version $FISH_VERSION
::OMP:: print transient --config $POSH_THEME --shell fish --error $omp_status_cache --execution-time $omp_duration --stack-count $omp_stack_count --shell-version $FISH_VERSION
return
end
set --global omp_status_cache $omp_status_cache_temp
@ -29,7 +29,7 @@ function fish_prompt
set --global --export omp_last_status_generation $status_generation
end
'::OMP::' print primary --config $POSH_THEME --shell fish --error $omp_status_cache --execution-time $omp_duration --stack-count $omp_stack_count --shell-version $FISH_VERSION
::OMP:: print primary --config $POSH_THEME --shell fish --error $omp_status_cache --execution-time $omp_duration --stack-count $omp_stack_count --shell-version $FISH_VERSION
end
function fish_right_prompt
@ -39,14 +39,14 @@ function fish_right_prompt
return
end
if test -n "$omp_tooltip_command"
set omp_tooltip_prompt ('::OMP::' print tooltip --config $POSH_THEME --shell fish --error $omp_status_cache --shell-version $FISH_VERSION --command $omp_tooltip_command)
set omp_tooltip_prompt (::OMP:: print tooltip --config $POSH_THEME --shell fish --error $omp_status_cache --shell-version $FISH_VERSION --command $omp_tooltip_command)
if test -n "$omp_tooltip_prompt"
echo -n $omp_tooltip_prompt
set omp_tooltip_command ""
return
end
end
'::OMP::' print right --config $POSH_THEME --shell fish --error $omp_status_cache --execution-time $omp_duration --stack-count $omp_stack_count --shell-version $FISH_VERSION
::OMP:: print right --config $POSH_THEME --shell fish --error $omp_status_cache --execution-time $omp_duration --stack-count $omp_stack_count --shell-version $FISH_VERSION
end
function postexec_omp --on-event fish_postexec

View file

@ -27,14 +27,14 @@ local tooltip_active = false
local cached_prompt = {}
local function omp_exe()
return [["::OMP::"]]
return '"'..::OMP::..'"'
end
local function omp_config()
return [["::CONFIG::"]]
return '"'..::CONFIG::..'"'
end
os.setenv("POSH_THEME", omp_config())
os.setenv("POSH_THEME", ::CONFIG::)
local function can_async()
if (clink.version_encoded or 0) >= 10030001 then

View file

@ -1,5 +1,5 @@
let-env POWERLINE_COMMAND = 'oh-my-posh'
let-env POSH_THEME = '::CONFIG::'
let-env POSH_THEME = ::CONFIG::
let-env PROMPT_INDICATOR = ""
# By default displays the right prompt on the first line
# making it annoying when you have a multiline prompt
@ -8,9 +8,9 @@ let-env PROMPT_COMMAND_RIGHT = {''}
let-env NU_VERSION = (version | get version)
# PROMPTS
let-env PROMPT_MULTILINE_INDICATOR = (^'::OMP::' print secondary $"--config=($env.POSH_THEME)" --shell=nu $"--shell-version=($env.NU_VERSION)")
let-env PROMPT_MULTILINE_INDICATOR = (^::OMP:: print secondary $"--config=($env.POSH_THEME)" --shell=nu $"--shell-version=($env.NU_VERSION)")
let-env PROMPT_COMMAND = {
let width = (term size -c | get columns | into string)
^'::OMP::' print primary $"--config=($env.POSH_THEME)" --shell=nu $"--shell-version=($env.NU_VERSION)" $"--execution-time=($env.CMD_DURATION_MS)" $"--error=($env.LAST_EXIT_CODE)" $"--terminal-width=($width)"
^::OMP:: print primary $"--config=($env.POSH_THEME)" --shell=nu $"--shell-version=($env.NU_VERSION)" $"--execution-time=($env.CMD_DURATION_MS)" $"--error=($env.LAST_EXIT_CODE)" $"--terminal-width=($width)"
}

View file

@ -15,14 +15,14 @@ function global:Get-PoshStackCount {
New-Module -Name "oh-my-posh-core" -ScriptBlock {
$script:ErrorCode = 0
$script:OMPExecutable = "::OMP::"
$script:OMPExecutable = ::OMP::
$script:ShellName = "::SHELL::"
$script:PSVersion = $PSVersionTable.PSVersion.ToString()
$script:TransientPrompt = $false
$env:POWERLINE_COMMAND = "oh-my-posh"
$env:CONDA_PROMPT_MODIFIER = $false
if (("::CONFIG::" -ne '') -and (Test-Path "::CONFIG::")) {
$env:POSH_THEME = (Resolve-Path -Path "::CONFIG::").ProviderPath
if ((::CONFIG:: -ne '') -and (Test-Path ::CONFIG::)) {
$env:POSH_THEME = (Resolve-Path -Path ::CONFIG::).ProviderPath
}
# specific module support (disabled by default)
if ($null -eq $env:POSH_GIT_ENABLED) {
@ -247,7 +247,7 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
|___/
'@
Write-Host $logo
$themes = Get-ChildItem -Path "$Path\*" -Include '*.omp.json' | Sort-Object Name
$themes = Get-ChildItem -Path "$Path/*" -Include '*.omp.json' | Sort-Object Name
if ($List -eq $true) {
$themes | Select-Object @{ Name = 'hyperlink'; Expression = { Get-FileHyperlink -uri $_.FullName } } | Format-Table -HideTableHeaders
} else {
@ -263,7 +263,7 @@ Themes location: $(Get-FileHyperlink -uri "$Path")
To change your theme, adjust the init script in $PROFILE.
Example:
oh-my-posh init pwsh --config $Path/jandedobbeleer.omp.json | Invoke-Expression
oh-my-posh init pwsh --config '$((Join-Path $Path "jandedobbeleer.omp.json") -replace "'", "''")' | Invoke-Expression
"@
}

View file

@ -1,4 +1,4 @@
export POSH_THEME='::CONFIG::'
export POSH_THEME=::CONFIG::
export POWERLINE_COMMAND="oh-my-posh"
export CONDA_PROMPT_MODIFIER=false
@ -64,7 +64,7 @@ if [[ "::TOOLTIPS::" = "true" ]]; then
zle -N self-insert
fi
_posh-zle-line-init() {
function _posh-zle-line-init() {
[[ $CONTEXT == start ]] || return 0
# Start regular line editor