2

I want to display the current working directory in my prompt differently if I am in a symlink than not.

I have this far:

[[ `pwd -P` = `pwd` ]] && echo "\[1;31m\]$(pwd -P)" || echo "\[1;32m\]$(pwd)"

will return the desired output but it will not work as a replacement for \w in the command prompt.

I tried wrapping it with backticks but that just results in pwd -Ppwd in the PS1.

I want to conditionally change the color and the value if it is a symlink or not which is why I want the if/else type decision.

  • "In a PS1 definition"? Show **exactly** how you're assigning to the variable. – Charles Duffy Apr 17 '18 at 15:45
  • ...btw, note that `fork()`ing three subshells every time you want to render a prompt is, from a performance perspective, Not Great. (Can't tell if you're *actually* doing that until the question is edited, though). I'd at least suggest caching the physical version until/unless a directory change is seen, and referring to `$PWD` to get the other without the expense of a subshell. – Charles Duffy Apr 17 '18 at 15:46
  • I believe putting the fancy stuff in a function is how it is usually done. Keeps your PS1 setting a bit more readable (for rather lax definitions of readable). – Mat Apr 17 '18 at 15:46
  • Using `PROMPT_COMMAND` to set `PS1` on demand is also usually more readable than trying to embed code in `PS1`. – chepner Apr 17 '18 at 15:49
  • BTW, `a && b || c` is not the same as `if a; then b; else c; fi`. Think about what happens if `a` is true, but `b` fails with an error: You can end up with both `a` and `c` running. – Charles Duffy Apr 17 '18 at 16:25

2 Answers2

5

If you want this to actually be efficient (and prompts should be fast to render!), then you'll want to cache the physical lookups, and use $PWD rather than $(pwd) for the logical ones.

# global variables are prefixed with funcname__ to avoid conflicts

# look up color codes for our terminal rather than assuming ANSI
set_prompt__red=$(tput setaf 1)
set_prompt__green=$(tput setaf 2)
set_prompt__reset=$(tput sgr0)

set_prompt() {

  # only rerun this code when changing directories!
  if [[ $set_prompt__lastPWDl != "$PWD" ]]; then
    set_prompt__lastPWDl=$PWD
    set_prompt__lastPWDp=$(pwd -P)
    if [[ "$set_prompt__lastPWDl" = "$set_prompt__lastPWDp" ]]; then
      set_prompt__pwd_color="$set_prompt__red"
    else
      set_prompt__pwd_color="$set_prompt__green"
    fi
  fi

  # ...actually could have just "return"ed above, but this way we can change other
  # aspects of the prompt even when we don't need to do a new directory lookup.
  PS1='...whatever prefix...'
  PS1+="\[${set_prompt__pwd_color}\]${PWD}\[${set_prompt__reset}\]"
  PS1+='...whatever suffix...'
}
PROMPT_COMMAND=set_prompt
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • you might want to localize the function's variables. – glenn jackman Apr 17 '18 at 16:07
  • @glennjackman, I thought about that, but not all of them *can be* localized. We're depending on persistence across invocations for caching, after all. Can use a prefix to create an effective namespace by convention, though. – Charles Duffy Apr 17 '18 at 16:12
  • you don't use the "PWDp" variable outside of that if statement, and the first thing you do is assign it. There's no reason it needs to be global. – glenn jackman Apr 17 '18 at 16:47
  • @glennjackman, sure, but that's the only one out of the set. I really can't bring myself to care at that point -- the consistency loss involved in changing it strikes me as a bigger maintainability hit than the namespace-containment, particularly with the variable names prefixed. Might as well maintain the flexibility to refer to it outside the `if` statement if the OP wants to use it in their PS1 construction (even in non-directory-changing cases) in the future.. – Charles Duffy Apr 17 '18 at 16:51
  • @glennjackman, ...see edit, making things a little DRYer: This way the user can switch from `$PWD` to `$set_prompt__lastPWDp` in the assignment outside the conditional if they want to change the behavior at hand. – Charles Duffy Apr 17 '18 at 16:58
  • everytime I `cd` into a `symlink` directory it prints out the `symlink` before the prompt. –  Apr 17 '18 at 18:42
  • Huh. I only get that when I `cd` into a symlink directory with `cd -`, but that's the normal behavior of `cd -`. See https://gist.github.com/charles-dyfis-net/888e8350481b24eeb52378bf066750c2 – Charles Duffy Apr 17 '18 at 19:48
2

This is what I ended up with:

I wanted to change the pwd value as well as the color when it is a symlink.

# look up color codes for our terminal rather than assuming ANSI
declare -r red=$(tput setaf 1)
declare -r green=$(tput setaf 2)
declare -r white=$(tput setaf 9)
declare -r aqua=$(tput setaf 6)
declare -r reset=$(tput sgr0)

colorize_msg() {
   printf -v $1 "\[%s\]%s" ${2} ${3}
}

set_prompt() {

    declare prompt_pwd=""
    # only rerun this code when changing directories!
    if [[ last_prompt_pwdL != $PWD ]]; then
        declare -g last_prompt_pwdL=$PWD # logical path
        declare -r last_prompt_pwdP=$(pwd -P) # physical path
        if [[ $last_prompt_pwdL = $last_prompt_pwdP ]]; then
          colorize_msg prompt_pwd $green $last_prompt_pwdL
        else
          colorize_msg prompt_pwd $red $last_prompt_pwdP
        fi

        # ...actually could have just "return"ed above, but this way we can change other
        # aspects of the prompt even when we don't need to do a new directory lookup.
        declare prompt=""
        declare msg=""
        colorize_msg msg $white "["
        prompt+=$msg
        colorize_msg msg $aqua "\u"
        prompt+=$msg
        colorize_msg msg $red "@"
        prompt+=$msg
        colorize_msg msg $aqua"\h"
        prompt+=$msg
        colorize_msg msg $white "] ["
        prompt+=$msg
        prompt+=${prompt_pwd}
        colorize_msg msg $white "]"
        prompt+=$msg
        prompt+="${reset}\n"
        PS1=$prompt
    fi
}
Community
  • 1
  • 1
  • Also, the subshells make this very inefficient. `var=$(echo "foo")` is **vastly** slower (multiple orders of magnitude) than `var=foo`. Even if you want to use a function, pass the function a variable name and have it append to that variable directly rather than running it in a subshell. That is to say, you can write more efficient code if your calling convention is `colorize_append_msg prompt "$red" "@"` rather than `prompt+=$(colorize_msg "$red" "@")`. And without the quotes around `"$red"`, you've got a bunch of bugs that'll fire if `$red` contains a character in the current value `IFS`. – Charles Duffy Apr 17 '18 at 19:38
  • `1)` I like immutablity, `2)` thanks for the advice I will update it when I get back to work and update my answer. –  Apr 17 '18 at 19:41
  • Re: (1) -- I like immutability too (when I'm not over here in the bash tag for entertainment purposes, my language of choice is Clojure), but your `prompt_pwd` value necessarily changes between invocations, so immutability is the wrong thing in this particular case. If you're depending on `declare` making it local, then you can't use the variable when the `if` doesn't fire. – Charles Duffy Apr 17 '18 at 19:42
  • ...btw, re: implementing that `colorize_append_msg`, see [BashFAQ #6](https://mywiki.wooledge.org/BashFAQ/006#Assigning_indirect.2Freference_variables) on indirect assignment. – Charles Duffy Apr 17 '18 at 19:44
  • btw, note that the exit status of `declare foo=$(false)` is true -- `declare`'s exit status overwrites the command substitution's exit status. For that reason, it's ideal to do your declarations as separate lines from your assignments. This is, btw, another issue that http://shellcheck.net/ will detect. – Charles Duffy Apr 17 '18 at 19:49