24
\$\begingroup\$

A long time ago I created a script for moving up directories very quickly in the command line using the command up. You can find usage notes here.

It's a very simple script with just 8 lines of source code, as follows:

if [ -z "$1" ]; then
    cd ..
else
    for i in `seq 1 $1`;
    do
        cd ..
    done
fi

I've never personally had any problems with it since I made it and started using it myself — but are there any accidental or malicious inputs (particularly with the blind injection of $1) that might cause this to do something bad that I'm not aware of?

Given that the up command isn't used for anything in any command line I'm aware of, I'd like to promote more widespread usage of this script file so that people can type up instead of cd .. all the time, saving three keystrokes (or more if they want to move up more directories) for a very common operation.

\$\endgroup\$
6
  • \$\begingroup\$ zsh: ... Also, ..., ...., ....., ... \$\endgroup\$
    – naught101
    Commented Oct 26, 2015 at 1:36
  • 1
    \$\begingroup\$ @naught101: that's why I tagged this question bash. :P \$\endgroup\$
    – Joe Z.
    Commented Oct 26, 2015 at 1:47
  • 2
    \$\begingroup\$ Nice approach, but I simply used alias f='cd ..;';alias ff='cd ..;cd ..;'alias fff='cd ..;cd ..;cd ..;';alias ffff='cd ..;cd ..;cd ..;cd ..;'; alias fffff='cd ..;cd ..;cd ..';cd ..;cd ..; in bashrc because there's only so many times you'd want to go up and the "f" key is easily accessible. Just a matter of typing "f" a few times and then hit enter instead of type up 5. The "f" stands for "fall" because from childhood, I considered cd dirName to be "climbing" directories and cd .. to be falling from a climbed directory. \$\endgroup\$
    – Nav
    Commented Oct 26, 2015 at 9:18
  • 3
    \$\begingroup\$ @Nav: if you were doing that, why not use cd ../.. for ff? You iterated them independently by hand. \$\endgroup\$
    – Joe Z.
    Commented Oct 26, 2015 at 14:56
  • \$\begingroup\$ I've been satisfied with just cd to take me home. Since all the files I own and work on are within there. And usually getting back to working files are less than four or five tab-competes deep. Though more commonly cd ~/where/ever/. [Just speaking generally about my personal cd usage.] \$\endgroup\$ Commented Oct 26, 2015 at 19:18

4 Answers 4

16
\$\begingroup\$

Your recommendation is to define alias up=". path/to/up" so that when you type up 3, it expands to . up 3. However, since you want to take an optional argument and affect the state of the current shell, I think you would be better off defining a shell function instead.

As it turns out, the [ -z "$1" ] special case is not necessary, since seq 1 just expands to 1.

You end up executing n separate cd .. commands for up n. This leads to a usability bug: cd - or cd $OLDPWD, which normally take you back to the previous directory, don't work the way I expect.

Suggested solution:

up() {
    cd $(for i in $(seq 1 $1) ; do echo -n ../ ; done)
}
\$\endgroup\$
3
  • \$\begingroup\$ So defining up in that way makes a literal cd - useless? Or were you expecting up - to work as cd - as well? \$\endgroup\$
    – Joe Z.
    Commented Oct 26, 2015 at 1:06
  • 5
    \$\begingroup\$ @JoeZ. the former - cd - will return you back one level after calling up n with n > 1. This causes an issue if you want to get back to where you were before you called up n. Concating the command into a single cd will preserve the functionality. \$\endgroup\$ Commented Oct 26, 2015 at 16:18
  • \$\begingroup\$ This is an extremely elegant solution and should be the accepted answer. Kudos. \$\endgroup\$
    – Wildcard
    Commented Nov 29, 2015 at 8:27
22
\$\begingroup\$

This is a decent concept, but by looping on the cd you loose some of the value of the $OLDPWD function in the shell. For example, I often use the special construct cd - in a shell, and that changes directory to the one you were in before.

Your code will make that impossible.

I would instead recommend that you instead build up a chain of ../ string values, like ../../../../ for 4 directories, and then just call cd once, which will preserve the cd - function, and the $OLDPWD.

Additionally, this would be a good feature to include as a function in your code, rather than a script. Bash shell likes functions, and they make life easier.

Finally, if someone supplies a non-number as an argument, it will do odd things.

I played with your code, and came up with:

up () {
    local count=$1
    if [ -z "$count" ]; then
        cd ..
        return
    fi

    test "$count" -eq "$count" || return 1

    local todir=""
    for i in `seq 1 $count`;
    do
      todir="../$todir"
    done
    cd $todir
}

The features of the above code I like are:

  • it is a function of the shell, so there's no additional script called.
  • it checks the value is a number, by doing a numeric comparison on the value: test "$count" -eq "$count" (That will throw an error if the inputs are not integers)
  • it only does a single 'cd', so things like cd - still work.

I would add that to my ~/.bashrc file, or source it in to my current shell.

\$\endgroup\$
0
11
\$\begingroup\$

200_success's answer uses the evil eval which in his context was unnecessary.

Update: Oh, what, ? Let's use some brace expansion + evil things:

# This time we have to be evil. All code in this answer is CC0.
up() { [ "$1" -eq "$1" ] &>/dev/null || set -- 1; "cd \$(printf '../%.s' {1..$1})"; }

local is not a that nice solution since POSIX doesn't contain local. After some searching, I found out that it's possible to use printf to duplicate strings. After replacing the brace expansion with seq, here is what I got:

up() { cd "$(printf '../%.s' $(seq 1 $1))"; }

P.S.: The use of seq for simple looping is sometimes considered harmful, especially when used with for var in. To make you feel less guilty:

# integer seq, @copyright CC0.
iseq() (
    : ${iseq_fmt='%s\n'}
    case "$#" in
        (1)  i=1  last=$1 incr=1;;
        (2)  i=$1 last=$2 incr=1;;
        (3)  i=$1 last=$3 incr=$2;;
        (*)  return 2;;
    esac
    [ "$i" -eq "$i" ] && [ "$incr" -eq "$incr" ] && [ "$last" -eq "$last" ] || return 2
    while [ "$i" -le "$last" ]; do
        printf "$iseq_fmt" "$i"
        : $((i = i + incr))
    done
)
\$\endgroup\$
3
  • \$\begingroup\$ In my solution, i is scoped to a subshell. The prior value of $i has no influence on the execution, nor does the execution alter the value of $i in the main shell. \$\endgroup\$ Commented Oct 26, 2015 at 0:38
  • \$\begingroup\$ @200_success Oh right, command substitution. \$\endgroup\$ Commented Oct 26, 2015 at 0:46
  • 1
    \$\begingroup\$ @200_success But you still don't need that eval actually. \$\endgroup\$ Commented Oct 26, 2015 at 0:47
0
\$\begingroup\$

You probably want to forward any options passed to cd, particularly -L, -P, -e and -@. Luckily, none of those take any arguments, so it's straightforward to catch them:

up() {
    local options=()
    for i in "$@"
    do
        case "$i" in
          -*) options+=("$i") ;;
          [1-9]*)
             test "$i" -eq "$i" &&
             cd "${options[@]}" $(perl -e "print'../'x$i;")
             return ;;
        esac
    done

    # If we got here, there were no non-option arguments
    cd "${options[@]}" ..
}

I might name it .. rather than up.

\$\endgroup\$
5
  • \$\begingroup\$ Do you want [1-9]* or ([1-9][0-9]*)?? If you typed up 10 what would happen? \$\endgroup\$
    – Joe Z.
    Commented Nov 8, 2018 at 23:35
  • \$\begingroup\$ I'm confusing them with regexes. Thanks for telling me. \$\endgroup\$
    – Joe Z.
    Commented Nov 11, 2018 at 3:22
  • \$\begingroup\$ Er, yes, I meant regexps - sorry for the confusion. Anyway, 10 matches (I tested it). \$\endgroup\$ Commented Nov 12, 2018 at 11:03
  • \$\begingroup\$ What happens if you type up 1P? \$\endgroup\$
    – Joe Z.
    Commented Nov 12, 2018 at 18:03
  • \$\begingroup\$ Then the test "$i" -eq "$i" fails and nothing happens (it would be nicer if it gave the user an error message; that's an exercise for the reader) \$\endgroup\$ Commented Nov 12, 2018 at 18:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.