Can you explain these three things in this bash code for me?

  1. d=$d/.. adds /.. to the current contents of the d variable. d starts off empty, then the first iteration makes it /.., the second /../.. etc.

  2. sed 's/^\///' drops the first /, so /../.. becomes ../.. (this can be done using a parameter expansion, d=${d#/}).

  3. d=.. only makes sense in the context of its condition:

    if [ -z "$d" ]; then
      d=..
    fi
    

    This ensures that, if d is empty at this point, you go to the parent directory. (up with no argument is equivalent to cd ...)

This approach is better than iterative cd .. because it preserves cd - — the ability to return to the previous directory (from the user’s perspective) in one step.

The function can be simplified:

up() {
  local d=..
  for ((i = 1; i < ${1:-1}; i++)); do d=$d/..; done
  cd $d
}

This assumes we want to move up at least one level, and adds n - 1 levels, so we don’t need to remove the leading / or check for an empty $d.

Using Athena jot (the athena-jot package in Debian):

up() { cd $(jot -b .. -s / "${1:-1}"); }

(based on a variant suggested by glenn jackman).


But can you explain these three things from it for me?

  1. d=$d/..

    This concatenates the present contents of var d with /.. and assign it back to d.
    The end result is to make d a repeated string like /../../../...

  2. sed 's/^///'

    Remove the leading / from the string supplied, d (echo $d) in the code you posted.
    Probably better written as sed 's|^/||' to avoid the backslash.

    An alternative (faster and simpler) is to write d=${d#/}.

  3. d=..

    Assign the string .. to the var d.
    This only makes sense as a way to ensure that d has at least one .. in case the test if [ -z "$d" ]; then signals that the var d is empty. And that can only happen because the sed command is removing one character from d.
    If there is no need to remove the character from d, there is no need for sed or if.


The code in your question will always move up at least one dir.

Better

  • local d is enough to make sure that the variable is empty, nothing else is needed.
    However, local works only in some shells like bash or dash. In specific, ksh (as yash) doesn't have a local command. A more portable solution is:

    [ "$KSH_VERSION$YASH_VERSION" ] && typeset c='' d='' i='' || local c='' d='' i=''
    
  • The for(( syntax is not portable. Better use something like:

    while [ "$((i+=1))" -lt "$limit" ]; do
    
  • Robust.
    If the values given to the function first argument could be negative or text, the function should be robust to process those values.
    First, lets limit the values to only numbers (I'll use c for count):

    c=${1%%[!0-9]*}
    

    And then limit the count value only to positive values:

    let "c = (c>0)?c:0"
    

This function will hapily accept 0 or any text (without errors):

up()
{
    [ "$KSH_VERSION$YASH_VERSION" ] && typeset c='' d='' i='' || local c='' d='' i=''
    c=${1%%[!0-9]*}
    c=$(((c>0)?c:0))
    while [ "$((i+=1))" -le "$c" ]; do d="$d../"; done
    echo \
        cd "${d%/}"         # Removing the trailing / is not really needed for cd
                            # but it is nice to know it could be done cleanly.
}

up 2
up 0
up asdf

Remove the echo \ when you have tested the function.