diff --git a/src/shell/init.go b/src/shell/init.go index 61fcd6af..eee16943 100644 --- a/src/shell/init.go +++ b/src/shell/init.go @@ -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) { diff --git a/src/shell/init_test.go b/src/shell/init_test.go index 6eae0d5c..5679878b 100644 --- a/src/shell/init_test.go +++ b/src/shell/init_test.go @@ -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)) + } +} diff --git a/src/shell/scripts/omp.bash b/src/shell/scripts/omp.bash index eac4bda7..645610b6 100644 --- a/src/shell/scripts/omp.bash +++ b/src/shell/scripts/omp.bash @@ -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") diff --git a/src/shell/scripts/omp.fish b/src/shell/scripts/omp.fish index dd1886dd..ed50e228 100644 --- a/src/shell/scripts/omp.fish +++ b/src/shell/scripts/omp.fish @@ -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 diff --git a/src/shell/scripts/omp.lua b/src/shell/scripts/omp.lua index 63b60340..9df35b64 100644 --- a/src/shell/scripts/omp.lua +++ b/src/shell/scripts/omp.lua @@ -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 diff --git a/src/shell/scripts/omp.nu b/src/shell/scripts/omp.nu index bad7cfa9..b30d22a4 100644 --- a/src/shell/scripts/omp.nu +++ b/src/shell/scripts/omp.nu @@ -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)" } diff --git a/src/shell/scripts/omp.ps1 b/src/shell/scripts/omp.ps1 index 9d6f555b..b4f50510 100644 --- a/src/shell/scripts/omp.ps1 +++ b/src/shell/scripts/omp.ps1 @@ -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 "@ } diff --git a/src/shell/scripts/omp.zsh b/src/shell/scripts/omp.zsh index 0151775f..86f44f70 100644 --- a/src/shell/scripts/omp.zsh +++ b/src/shell/scripts/omp.zsh @@ -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