Search code examples
unixpipezshlscd

Unix: (pwd now: ~) from cd command?


Me being me, I went playing around with the terminal (zsh.) One of the things I tried (in an empty directory) was mkdir test; ls | cd This should make a folder then print all folders (so just that one) and pipe the output to cd. So I expected it to take me to the test directory but instead I got (pwd now: ~) and it took me back to the home directory. Does anyone know why this is? Looking it up gives nothing useful.

EDIT: It takes me to the ~ directory because cd doesn't accept stdin inputs, but rather the directory is an argument. What I still want to know is why running ls | cd gives the output (pwd now: ~) instead of just nothing. Running echo test | cd gives no output but ls | cd does.


Solution

  • When you "pipe" input from one process or job to another, you are also spawning another process. Both of these jobs are said to be "children" of main process which called them (usually the process running the terminal emulator).

    # 2 process are run here
    # first job: echo
    # second job: ls
    echo "/home/" | ls
    

    Looking at the source for zsh, it looks like when the cwd of a job differs from the original cwd of the job after execution, it will notify the user with a message like (pwd now: foo). This helps to ensure that the user knows exactly where the are in the directory tree, which is especially useful when they may not have intended to do so. Below is taken from jobs.c where the cwd (referred to as pwd) where among other things, the cwd (referenced as pwd) are compared before printing the (pwd now: foo) message. If they are different the message is printed, if they are equal it is not.

    if ((lng & 4) || (interact && job == thisjob &&
                  jn->pwd && strcmp(jn->pwd, pwd))) {
        doneprint = 1;
        fprintf(fout, "(pwd %s: ", (lng & 4) ? "" : "now");
        fprintdir(((lng & 4) && jn->pwd) ? jn->pwd : pwd, fout);
        fprintf(fout, ")\n");
        fflush(fout);
        }
    

    When you pipe something into ch you are changing the cwd in a child process and some of the normal checks which hide this message when calling cd directly are bipassed. Otherwise cd words in the same way.

    # changes directories into ./test
    echo "test" | cd
    # (pwd now: /path/to/test)
    
    # changes directory to home
    echo | cd
    # (pwd now: /home/<username>)
    
    # calling this again will not echo the message, since the cwd is already
    # the home directory and no change occurs 
    echo | cd
    

    As for why the cwd is changed to the home directory (~) it is due to how cd behaves when no paths are given as an argument. Unlike a lot of linux commands, cd does not read from stdin for paths to move into. Due to this, piping into cd will simply populate stdin for cd, but that content is ignored. Because of this piping into cd is the same as just calling cd on its own.

    # these next lines produce the same behavior
    echo path/to/dir/test | cd
    cd
    

    When cd does not receive a path to move to, it will move you to your home directory (referenced on linux systems as ~)

    # each of these lines produce the same behavior
    cd /home/<username>
    cd ~
    cd