mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-11-09 20:44:03 -08:00
fix(shell): avoid unexpected expansions in Bash/Zsh
This commit is contained in:
parent
c289a038ac
commit
e2626c8668
|
@ -2,7 +2,6 @@ package prompt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jandedobbeleer/oh-my-posh/src/color"
|
"github.com/jandedobbeleer/oh-my-posh/src/color"
|
||||||
"github.com/jandedobbeleer/oh-my-posh/src/config"
|
"github.com/jandedobbeleer/oh-my-posh/src/config"
|
||||||
|
@ -101,17 +100,15 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
|
||||||
|
|
||||||
switch e.Env.Shell() {
|
switch e.Env.Shell() {
|
||||||
case shell.ZSH:
|
case shell.ZSH:
|
||||||
// escape double quotes contained in the prompt
|
|
||||||
if promptType == Transient {
|
if promptType == Transient {
|
||||||
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(str, "\"", "\"\""))
|
prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(str))
|
||||||
// empty RPROMPT
|
// empty RPROMPT
|
||||||
prompt += "\nRPROMPT=\"\""
|
prompt += "\nRPROMPT=''"
|
||||||
return prompt
|
return prompt
|
||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
case shell.PWSH, shell.PWSH5:
|
case shell.PWSH, shell.PWSH5:
|
||||||
if promptType == Transient {
|
if promptType == Transient {
|
||||||
// Return the string and empty our buffer
|
|
||||||
// clear the line afterwards to prevent text from being written on the same line
|
// clear the line afterwards to prevent text from being written on the same line
|
||||||
// see https://github.com/JanDeDobbeleer/oh-my-posh/issues/3628
|
// see https://github.com/JanDeDobbeleer/oh-my-posh/issues/3628
|
||||||
return str + terminal.ClearAfter()
|
return str + terminal.ClearAfter()
|
||||||
|
@ -119,7 +116,6 @@ func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
|
||||||
|
|
||||||
return str
|
return str
|
||||||
case shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.GENERIC:
|
case shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.GENERIC:
|
||||||
// Return the string and empty our buffer
|
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,12 @@ func (e *Engine) Primary() string {
|
||||||
// Warp doesn't support RPROMPT so we need to write it manually
|
// Warp doesn't support RPROMPT so we need to write it manually
|
||||||
if e.isWarp() {
|
if e.isWarp() {
|
||||||
e.writePrimaryRightPrompt()
|
e.writePrimaryRightPrompt()
|
||||||
// escape double quotes contained in the prompt
|
prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(e.string()))
|
||||||
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
|
|
||||||
return prompt
|
return prompt
|
||||||
}
|
}
|
||||||
|
|
||||||
// escape double quotes contained in the prompt
|
prompt := fmt.Sprintf("PS1=%s", shell.QuotePosixStr(e.string()))
|
||||||
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
|
prompt += fmt.Sprintf("\nRPROMPT=%s", shell.QuotePosixStr(e.rprompt))
|
||||||
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
|
|
||||||
|
|
||||||
return prompt
|
return prompt
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (f Feature) Bash() Code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func quotePosixStr(str string) string {
|
func QuotePosixStr(str string) string {
|
||||||
if len(str) == 0 {
|
if len(str) == 0 {
|
||||||
return "''"
|
return "''"
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ type Formats struct {
|
||||||
Osc7 string
|
Osc7 string
|
||||||
Osc51 string
|
Osc51 string
|
||||||
|
|
||||||
EscapeSequences map[rune]rune
|
EscapeSequences map[rune]string
|
||||||
|
|
||||||
HyperlinkStart string
|
HyperlinkStart string
|
||||||
HyperlinkCenter string
|
HyperlinkCenter string
|
||||||
|
@ -50,9 +50,8 @@ func GetFormats(shell string) *Formats {
|
||||||
ITermPromptMark: "\\[$(iterm2_prompt_mark)\\]",
|
ITermPromptMark: "\\[$(iterm2_prompt_mark)\\]",
|
||||||
ITermCurrentDir: "\\[\x1b]1337;CurrentDir=%s\x07\\]",
|
ITermCurrentDir: "\\[\x1b]1337;CurrentDir=%s\x07\\]",
|
||||||
ITermRemoteHost: "\\[\x1b]1337;RemoteHost=%s@%s\x07\\]",
|
ITermRemoteHost: "\\[\x1b]1337;RemoteHost=%s@%s\x07\\]",
|
||||||
EscapeSequences: map[rune]rune{
|
EscapeSequences: map[rune]string{
|
||||||
96: 92, // backtick
|
'\\': `\\`,
|
||||||
92: 92, // backslash
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case ZSH, TCSH:
|
case ZSH, TCSH:
|
||||||
|
@ -100,9 +99,8 @@ func GetFormats(shell string) *Formats {
|
||||||
}
|
}
|
||||||
|
|
||||||
if shell == ZSH {
|
if shell == ZSH {
|
||||||
formats.EscapeSequences = map[rune]rune{
|
formats.EscapeSequences = map[rune]string{
|
||||||
96: 92, // backtick
|
'%': "%%",
|
||||||
37: 37, // %
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,12 +93,12 @@ func PrintInit(env runtime.Environment, features Features, startTime *time.Time)
|
||||||
configFile = quotePwshOrElvishStr(configFile)
|
configFile = quotePwshOrElvishStr(configFile)
|
||||||
script = pwshInit
|
script = pwshInit
|
||||||
case ZSH:
|
case ZSH:
|
||||||
executable = quotePosixStr(executable)
|
executable = QuotePosixStr(executable)
|
||||||
configFile = quotePosixStr(configFile)
|
configFile = QuotePosixStr(configFile)
|
||||||
script = zshInit
|
script = zshInit
|
||||||
case BASH:
|
case BASH:
|
||||||
executable = quotePosixStr(executable)
|
executable = QuotePosixStr(executable)
|
||||||
configFile = quotePosixStr(configFile)
|
configFile = QuotePosixStr(configFile)
|
||||||
script = bashInit
|
script = bashInit
|
||||||
case FISH:
|
case FISH:
|
||||||
executable = quoteFishStr(executable)
|
executable = quoteFishStr(executable)
|
||||||
|
@ -113,8 +113,8 @@ func PrintInit(env runtime.Environment, features Features, startTime *time.Time)
|
||||||
configFile = quoteNuStr(configFile)
|
configFile = quoteNuStr(configFile)
|
||||||
script = nuInit
|
script = nuInit
|
||||||
case TCSH:
|
case TCSH:
|
||||||
executable = quotePosixStr(executable)
|
executable = QuotePosixStr(executable)
|
||||||
configFile = quotePosixStr(configFile)
|
configFile = QuotePosixStr(configFile)
|
||||||
script = tcshInit
|
script = tcshInit
|
||||||
case ELVISH:
|
case ELVISH:
|
||||||
executable = quotePwshOrElvishStr(executable)
|
executable = quotePwshOrElvishStr(executable)
|
||||||
|
|
|
@ -35,7 +35,7 @@ func TestQuotePosixStr(t *testing.T) {
|
||||||
{str: `C:/tmp/omp's dir/oh-my-posh.exe`, expected: `$'C:/tmp/omp\'s dir/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 {
|
for _, tc := range tests {
|
||||||
assert.Equal(t, tc.expected, quotePosixStr(tc.str), fmt.Sprintf("quotePosixStr: %s", tc.str))
|
assert.Equal(t, tc.expected, QuotePosixStr(tc.str), fmt.Sprintf("quotePosixStr: %s", tc.str))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,9 @@ _omp_ftcs_marks=0
|
||||||
|
|
||||||
# start timer on command start
|
# start timer on command start
|
||||||
PS0='${_omp_start_time:0:$((_omp_start_time="$(_omp_start_timer)",0))}$(_omp_ftcs_command_start)'
|
PS0='${_omp_start_time:0:$((_omp_start_time="$(_omp_start_timer)",0))}$(_omp_ftcs_command_start)'
|
||||||
|
|
||||||
# set secondary prompt
|
# set secondary prompt
|
||||||
PS2="$("$_omp_executable" print secondary --shell=bash --shell-version="$BASH_VERSION")"
|
_omp_secondary_prompt=$("$_omp_executable" print secondary --shell=bash --shell-version="$BASH_VERSION")
|
||||||
|
|
||||||
function _omp_set_cursor_position() {
|
function _omp_set_cursor_position() {
|
||||||
# not supported in Midnight Commander
|
# not supported in Midnight Commander
|
||||||
|
@ -58,7 +59,38 @@ function set_poshcontext() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
# regular prompt
|
function _omp_print_primary() {
|
||||||
|
# Avoid unexpected expansions.
|
||||||
|
shopt -u promptvars
|
||||||
|
|
||||||
|
local prompt
|
||||||
|
if shopt -oq posix; then
|
||||||
|
# Disable in POSIX mode.
|
||||||
|
prompt='[NOTICE: Oh My Posh prompt is not supported in POSIX mode]\n\u@\h:\w\$ '
|
||||||
|
else
|
||||||
|
prompt=$("$_omp_executable" print primary --shell=bash --shell-version="$BASH_VERSION" --status="$_omp_status_cache" --pipestatus="${_omp_pipestatus_cache[*]}" --execution-time="$_omp_elapsed" --stack-count="$_omp_stack_count" --no-status="$_omp_no_exit_code" --terminal-width="${COLUMNS-0}" | tr -d '\0')
|
||||||
|
fi
|
||||||
|
echo "${prompt@P}"
|
||||||
|
|
||||||
|
# Allow command substitution in PS0.
|
||||||
|
shopt -s promptvars
|
||||||
|
}
|
||||||
|
|
||||||
|
function _omp_print_secondary() {
|
||||||
|
# Avoid unexpected expansions.
|
||||||
|
shopt -u promptvars
|
||||||
|
|
||||||
|
if shopt -oq posix; then
|
||||||
|
# Disable in POSIX mode.
|
||||||
|
echo '> '
|
||||||
|
else
|
||||||
|
echo "${_omp_secondary_prompt@P}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Allow command substitution in PS0.
|
||||||
|
shopt -s promptvars
|
||||||
|
}
|
||||||
|
|
||||||
function _omp_hook() {
|
function _omp_hook() {
|
||||||
_omp_status_cache=$? _omp_pipestatus_cache=(${PIPESTATUS[@]})
|
_omp_status_cache=$? _omp_pipestatus_cache=(${PIPESTATUS[@]})
|
||||||
|
|
||||||
|
@ -82,7 +114,8 @@ function _omp_hook() {
|
||||||
set_poshcontext
|
set_poshcontext
|
||||||
_omp_set_cursor_position
|
_omp_set_cursor_position
|
||||||
|
|
||||||
PS1="$("$_omp_executable" print primary --shell=bash --shell-version="$BASH_VERSION" --status="$_omp_status_cache" --pipestatus="${_omp_pipestatus_cache[*]}" --execution-time="$_omp_elapsed" --stack-count="$_omp_stack_count" --no-status="$_omp_no_exit_code" --terminal-width="${COLUMNS-0}" | tr -d '\0')"
|
PS1='$(_omp_print_primary)'
|
||||||
|
PS2='$(_omp_print_secondary)'
|
||||||
|
|
||||||
return $_omp_status_cache
|
return $_omp_status_cache
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ _omp_cursor_positioning=0
|
||||||
_omp_ftcs_marks=0
|
_omp_ftcs_marks=0
|
||||||
|
|
||||||
# set secondary prompt
|
# set secondary prompt
|
||||||
PS2="$($_omp_executable print secondary --shell=zsh)"
|
_omp_secondary_prompt=$($_omp_executable print secondary --shell=zsh)
|
||||||
|
|
||||||
function _omp_set_cursor_position() {
|
function _omp_set_cursor_position() {
|
||||||
# not supported in Midnight Commander
|
# not supported in Midnight Commander
|
||||||
|
@ -73,6 +73,15 @@ function _omp_precmd() {
|
||||||
set_poshcontext
|
set_poshcontext
|
||||||
_omp_set_cursor_position
|
_omp_set_cursor_position
|
||||||
|
|
||||||
|
# We do this to avoid unexpected expansions in a prompt string.
|
||||||
|
unsetopt PROMPT_SUBST
|
||||||
|
unsetopt PROMPT_BANG
|
||||||
|
|
||||||
|
# Ensure that escape sequences work in a prompt string.
|
||||||
|
setopt PROMPT_PERCENT
|
||||||
|
|
||||||
|
PS2=$_omp_secondary_prompt
|
||||||
|
|
||||||
eval "$($_omp_executable print primary --status="$_omp_status_cache" --pipestatus="${_omp_pipestatus_cache[*]}" --execution-time="$_omp_elapsed" --stack-count="$_omp_stack_count" --eval --shell=zsh --shell-version="$ZSH_VERSION" --no-status="$_omp_no_exit_code")"
|
eval "$($_omp_executable print primary --status="$_omp_status_cache" --pipestatus="${_omp_pipestatus_cache[*]}" --execution-time="$_omp_elapsed" --stack-count="$_omp_stack_count" --eval --shell=zsh --shell-version="$ZSH_VERSION" --no-status="$_omp_no_exit_code")"
|
||||||
unset _omp_start_time
|
unset _omp_start_time
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,6 @@ var (
|
||||||
isInvisible bool
|
isInvisible bool
|
||||||
isHyperlink bool
|
isHyperlink bool
|
||||||
|
|
||||||
lastRune rune
|
|
||||||
|
|
||||||
Shell string
|
Shell string
|
||||||
Program string
|
Program string
|
||||||
|
|
||||||
|
@ -187,24 +185,34 @@ func ClearAfter() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func FormatTitle(title string) string {
|
func FormatTitle(title string) string {
|
||||||
|
// These shells don't support setting the console title.
|
||||||
|
if Shell == shell.ELVISH || Shell == shell.XONSH {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
title = trimAnsi(title)
|
title = trimAnsi(title)
|
||||||
|
|
||||||
if Plain {
|
if Plain {
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
// we have to do this to prevent bash/zsh from misidentifying escape sequences
|
if Shell != shell.BASH && Shell != shell.ZSH {
|
||||||
switch Shell {
|
return fmt.Sprintf(formats.Title, title)
|
||||||
case shell.BASH:
|
|
||||||
title = strings.NewReplacer("`", "\\`", `\`, `\\`).Replace(title)
|
|
||||||
case shell.ZSH:
|
|
||||||
title = strings.NewReplacer("`", "\\`", `%`, `%%`).Replace(title)
|
|
||||||
case shell.ELVISH, shell.XONSH:
|
|
||||||
// these shells don't support setting the title
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(formats.Title, title)
|
// We have to do this to prevent Bash/Zsh from misidentifying escape sequences.
|
||||||
|
s := new(strings.Builder)
|
||||||
|
for _, char := range title {
|
||||||
|
escaped, shouldEscape := formats.EscapeSequences[char]
|
||||||
|
if shouldEscape {
|
||||||
|
s.WriteString(escaped)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteRune(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(formats.Title, s.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func EscapeText(text string) string {
|
func EscapeText(text string) string {
|
||||||
|
@ -392,17 +400,18 @@ func write(s rune) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !Interactive {
|
// UNSOLVABLE: When "Interactive" is true, the prompt length calculation in Bash/Zsh can be wrong, since the final string expansion is done by shells.
|
||||||
for special, escape := range formats.EscapeSequences {
|
length += runewidth.RuneWidth(s)
|
||||||
if s == special && lastRune != escape {
|
// length += utf8.RuneCountInString(string(s))
|
||||||
builder.WriteRune(escape)
|
|
||||||
}
|
if !Interactive && !Plain {
|
||||||
|
escaped, shouldEscape := formats.EscapeSequences[s]
|
||||||
|
if shouldEscape {
|
||||||
|
builder.WriteString(escaped)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// length += utf8.RuneCountInString(string(s))
|
|
||||||
length += runewidth.RuneWidth(s)
|
|
||||||
lastRune = s
|
|
||||||
builder.WriteRune(s)
|
builder.WriteRune(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -287,8 +287,6 @@ func TestWriteLength(t *testing.T) {
|
||||||
CurrentColors = tc.Colors
|
CurrentColors = tc.Colors
|
||||||
Colors = &color.Defaults{}
|
Colors = &color.Defaults{}
|
||||||
|
|
||||||
Init(shell.GENERIC)
|
|
||||||
|
|
||||||
Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
|
Write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
|
||||||
|
|
||||||
_, got := String()
|
_, got := String()
|
||||||
|
|
|
@ -50,11 +50,20 @@ understand how to configure a segment.
|
||||||
| `templates` | `[]Template` | in some cases having a single [template][templates] string is a bit cumbersome. Templates allows you to span the segment's [template][templates] string multiple lines where every [template][templates] is evaluated and depending on what you aim to achieve, there are two possible outcomes based on `templates_logic` |
|
| `templates` | `[]Template` | in some cases having a single [template][templates] string is a bit cumbersome. Templates allows you to span the segment's [template][templates] string multiple lines where every [template][templates] is evaluated and depending on what you aim to achieve, there are two possible outcomes based on `templates_logic` |
|
||||||
| `templates_logic` | `string` | <ul><li>`first_match`: return the first non-whitespace string and skip everything else</li><li>`join`:evaluate all templates and join all non-whitespace strings (**default**)</li></ul> |
|
| `templates_logic` | `string` | <ul><li>`first_match`: return the first non-whitespace string and skip everything else</li><li>`join`:evaluate all templates and join all non-whitespace strings (**default**)</li></ul> |
|
||||||
| `properties` | `[]Property` | see [Properties][properties] below |
|
| `properties` | `[]Property` | see [Properties][properties] below |
|
||||||
| `interactive` | `boolean` | when is true, the segment text is not escaped to allow the use of interactive prompt escape sequences - defaults to `false` |
|
| `interactive` | `boolean` | when is true, the segment text is not escaped to allow the use of interactive prompt escape sequences in Bash/Zsh - defaults to `false` |
|
||||||
| `alias` | `string` | for use with [cross segment template properties][cstp] |
|
| `alias` | `string` | for use with [cross segment template properties][cstp] |
|
||||||
| `min_width` | `int` | if the terminal width is smaller than this value, the segment will be hidden. For your terminal width, see `oh-my-posh get width`. Defaults to `0` (disable) |
|
| `min_width` | `int` | if the terminal width is smaller than this value, the segment will be hidden. For your terminal width, see `oh-my-posh get width`. Defaults to `0` (disable) |
|
||||||
| `max_width` | `int` | if the terminal width exceeds this value, the segment will be hidden. For your terminal width, see `oh-my-posh get width`. Defaults to `0` (disable) |
|
| `max_width` | `int` | if the terminal width exceeds this value, the segment will be hidden. For your terminal width, see `oh-my-posh get width`. Defaults to `0` (disable) |
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
In Bash/Zsh, when the property `interactive` is `true` for a segment, the prompt length calculation can be wrong
|
||||||
|
because of possible string expansions (e.g., `\w` in Bash and `%d` in Zsh which both expand to the current working
|
||||||
|
directory), thus a right-aligned block is not being properly right-aligned.
|
||||||
|
|
||||||
|
Unfortunately, it's not possible for Oh My Posh to know the final prompt length since the string expansion is done
|
||||||
|
by your shell, so use this at your own risk.
|
||||||
|
:::
|
||||||
|
|
||||||
## Style
|
## Style
|
||||||
|
|
||||||
Style defines how a prompt is rendered. Looking at the most prompt
|
Style defines how a prompt is rendered. Looking at the most prompt
|
||||||
|
|
Loading…
Reference in a new issue