Search code examples
shellunixprocesspipecd

Piping `cd` or `popd` output prevents changing directories?


I understand that since | initiates a new process for the command(s) after the pipe, any shell command of the form cmd | cd newdir (where cmd does not change the current working directory) will leave the original process's working directory unchanged. (Not to mention that this is a bit silly since cd doesn't read input from stdin.)

However, on my machine (a CentOS 6 box, using bash, ksh, or zsh), it appears that the following command also fails to change directories:

cd newdir | cat

(Please ignore how silly it is to pipe output to cat here; I'm just trying to make a simple example.)

Why is this? Is there a way around this problem? Specifically, I'm trying to write an alias that uses popd but catches the output, discards stdout, and re-outputs stderr.

(For the curious, this is my current, non-working alias: popd 2>&1 >/dev/null | toerr && lsd. Here, toerr just catches stdin, outputs it to stderr, and returns the number of lines read/printed. lsd is a directory-name-and-contents printer that should only execute if the popd is successful. The reason I'm sending stderr to stdout, piping it, catching it, and re-outputting it on stderr is just to get it colored red using stderred, since my shell session isn't loaded with LD_PRELOAD, so Bash built-ins such as popd don't get the red-colored stderr.)


Solution

  • In bash, dash and ash, each command in a pipeline runs in a subshell.

    In zsh, ksh, and bash with shopt -s lastpipe, all except the last command in the pipeline run in subshells.

    Since cd -- as well as variables, shell options, ulimits and new file descriptors -- only affects the current process, their effects will not affect the parent shell.

    Examples:

    # Doesn't change directory
    cd foo | cat
    pwd
    
    # Doesn't set $bar on default bash (but does on zsh and ksh)
    echo foo | read bar
    echo "$bar"
    
    # Doesn't change the ulimit
    ulimit -c 10000 2>&1 | grep "not permitted"
    ulimit -c
    

    The same also applies to other things that generate subshells. None of the following will change the directory:

    # Command expansion creates a subshell
    echo $(cd foo); pwd
    
    # ( .. ) creates a subshell
    ( cd foo ); pwd
    
    # Backgrounding a process creates a subshell
    cd foo & pwd
    

    To fix it, you have to rewrite your code to run anything that affects the environment in the main shell process.

    In your particular case, you can consider using process substitution:

    popd > /dev/null 2>   >(toerr) && lsd
    

    This has the additional benefit of only running lsd when popd is successful, rather than when toerr is successful like your version does.