1

Good ol' -bash: cd ..: command not found.

I have a script I wrote to traverse up to a named directory. It had been working and now for the life of me I don't know what's changed.

$ type up
up is a function
up () 
{ 
    local arg="$@";
    if [ -z "$arg" ]; then
        cd ..;
        return 1;
    fi;
    local arr=();
    local sep='/';
    local IFS="$sep";
    local DIRS;
    read -ra DIRS <<< "$(pwd)";
    for ((i=${#DIRS[@]}-1; i>=0; i-- ))
    do
        if [ "${DIRS[i]}" = "$arg" ]; then
            local first=${arr[0]};
            arr=("${arr[@]:1}");
            $(printf "%s" "cd $first" "${arr[@]/#/$sep}" && echo "/");
            return 1;
        else
            arr+=('..');
        fi;
    done;
    echo "Directory \"$arg\" could not be found in path. Check spelling and try again." 1>&2;
    return 0
}

Steps

  1. if no arg is passed, go up a directory
  2. if an arg is passed:
    1. grab the pwd and split on /

    2. iterate over the list of dirs walking from the back of the queue

    3. if the current dir doesn't match the arg, add .. to a list

    4. if the dir matches the command line arg, unpack .., delimit with /, and execute the string as a command

      printf "%s" "cd $first" "${arr[@]/#/$sep}" && echo "/"
      
      # printf "%s" "cd $first" :   prints 'cd ..' (command & the first placeholder) 
      # "${arr[@]/#/$sep}"      :   prints the remaining '..' joined by '/'
      # echo "/"                :   adds a trailing '/' to the relative path 
      #                               which could have been included in the 
      #                               previous step but separated for comprehension
      
      # Given, pwd = /Users/dev/workspace/project/foo
      # `up workspace` navigates to 'cd ../../'
      

Debugging (from the terminal)

> cd ..
# works as expected

> $(echo "cd ..")
# works as expected --> maybe something with the "/" is breaking it?

> $('cd ..')
# -bash: cd ..: command not found

> $('cd ../')
# -bash: cd ../: No such file or directory

> which cd
# /usr/bin/cd

> echo $PATH
# /usr/bin

> type cd
# cd is a shell builtin

> alias cd
# -bash: alias: cd: not found

> shopt expand_aliases
# expand_aliases    on

Unsetting most of the environment variables (e.g., CDPATH) or opening a shell with a clean environment (i.e. env -i bash --noprofile --norc) did not help in bash3.2 on OSX.

Comments

My bash is a little rusty, so feel free to give pointers on a better way, that's as maintainable. I know return may not be necessary (or correct) and I've seen some find implementations, which were probably more efficient, but more difficult to follow.

7
  • if you want to execute cd .. to change up one directory, then do that, don't wrap it in a subshell to capture its output in a string, which is what $( ) does. Jan 15 at 18:41
  • Hi @MarcusMüller, I do that in the first if block, but in the remaining logic it can have a variable amount of relative paths (see last comment in Step #4 for an example). It might be cd ../../../../../.
    – Mike
    Jan 15 at 18:54
  • 1
    Like Marcus said, those command substitutions aren't helpful. Just build the directory name in a variable (e.g. with dir=$(printf ...), or printf -v dir ...), and then run cd "$dir".
    – ilkkachu
    Jan 15 at 18:55
  • Hi @ilkkachu I'm having trouble following or maybe I left an error in the example (I've been bouncing around). The effective code that it tries to run is something like $(echo "cd ../../../") which results in cd ..: command not found. The debug statements are just for confirmation. I've also tried outputting to a variable with -v out and that didn't prove fruitful. I'm partially troubled because this was working at one point.
    – Mike
    Jan 15 at 18:56
  • I don't think I saved to a variable without the command and cd-ing on the path. I'll try that!
    – Mike
    Jan 15 at 18:58

1 Answer 1

4

$(echo "cd ..") runs the shell command echo "cd .." (that is, the command echo with the single argument cd ..), takes the output (cd ..), splits that on whitespace (and globs, though that doesn't matter here), gets the two words cd and .., and runs the cd with the argument ... That's a convoluted and error-prone way of just running the shell command cd ...

Except that here, you've set IFS to a slash, so the word splitting part doesn't split on whitespace, but on slashes.

So, when you do e.g. $(echo "cd ../.."), the result of the command substitution is split not as cd, ../.., but as cd .., ... The space now being part of the command name here as if you entered "cd .." .. on the shell command line.

It's likely better not to rely on word-splitting to get it right (especially since it'll break the moment your directory names contain whitespace), but to keep the command and argument separate. Build the directory name in a variable and use that, i.e.:

dir=$(printf "../..")
cd -- "$dir" 

Or perhaps rather

printf -v dir "../.."
cd -- "$dir" 

Also, you might not need that printf there. You can use "${arr[*]}" to get the array elements joined with the first character of IFS, so with IFS=/, it would join them with slashes.

E.g.:

arr=(.. .. ..)
IFS=/
dir="${arr[*]}"

sets dir to ../../...

7
  • That makes much more sense to me. I was refactoring the IFS for local scoping. Per your recommendation, I still prefer to stick the output in a variable, but I knew what I had (cd as part of the print string) was working and I couldn't figure out why it broke. Thank you so much. Now I can get back to actual work after this this 2 hour detour :) Can I buy you a cup of coffee?
    – Mike
    Jan 15 at 19:11
  • It seems I need 15 creds to upvote. I'll make a todo to come back to boost this answer too.
    – Mike
    Jan 15 at 19:18
  • @Mike, haha, I did once think about getting a buymeacoffee.com account or such, just to see what would happen. But then I couldn't be bothered to actually do it. Don't worry about the upvotes either, writing here is just a hobby. (Ok, well, maybe it also has to do with an addiction induced by gamification and imaginary internet points, but anyway...)
    – ilkkachu
    Jan 15 at 19:31
  • @Mike, didn't you day in your question that it had been working before and you don't know what changed? So you did change something, you just didn't think it was relevant?
    – aviro
    Jan 16 at 8:36
  • @aviro It's not that I didn't think it was relevant, but that I didn't think anything had changed. In this case, it's been some time since I've done bash scripting, I needed a duck or another set of eyes -- expert eyes -- of you all here. I had briefly coded it up on OSX and Linux -- not repo checked (first mistake) -- and never encountered an error on either. When I came back to OSX, I started experiencing the error. At that point I don't think I modified anything. Was I using it differently?--I'm not sure.
    – Mike
    Jan 16 at 18:58

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .