简体   繁体   中英

Why pipe resets current working directory?

First script:

$ { mkdir dir; cd dir; pwd; } | cat; pwd;
./dir
.

Second script:

$ { mkdir dir; cd dir; pwd; }; pwd;
./dir
./dir

Why this | cat | cat has an effect on current directory? And how to solve it? I need the first script to work exactly as the second one. I don't want cat to change my current directory back to . .

Quoting from the manual :

Each command in a pipeline is executed in its own subshell (see Command Execution Environment ).

Also see Grouping Commands :

{}

  { list; } 

Placing a list of commands between curly braces causes the list to be executed in the current shell context. No subshell is created.

It's not that the pipe goes back to the directory, it's that you've made the first command (prior to the semicolon) applicable only to the cat command. You're essentially piping the output of the subprocess of the mkdir and cd and pwd go to the cat process.

For example: { mkdir dir; cd dir; pwd; } | cat; pwd; { mkdir dir; cd dir; pwd; } | cat; pwd;

First expands into two processes: 1) { mkdir dir; cd dir; pwd; } | cat; { mkdir dir; cd dir; pwd; } | cat; and 2) pwd

The first process expands into two processes, { mkdir dir; cd dir; pwd; } { mkdir dir; cd dir; pwd; } { mkdir dir; cd dir; pwd; } which then sends its stdout to stdin of cat . When the first of these two processes finishes and the stdout is collected, its subprocess exits and it is like the cd never happened because the cd only affects the directory of the process it was running in. The pwd never actually changed $PWD , it only printed that which was provided on stdin .

To resolve this issue (assuming I understand what you are trying to do) I would change this to:

{ mkdir dir; cd dir; pwd; }; pwd; cd -

When you run:

{ mkdir -p dir; cd dir; pwd; } | cat; pwd

OR

{ mkdir -p dir; cd dir; pwd; } | date

OR

{ mkdir -p dir; cd dir; pwd; } | ls

You are running group of commands on LHS of pipe in a sub-shell and hence change dir isn't reflected in current shell after both commands (LHS and RHS) complete.

However when you run:

{ mkdir -p dir; cd dir; pwd; }; pwd;

There is no pipe in between hence all the commands inside curly braces and pwd outside curly brace run in the current shell itself hence you get changed directory.

PS: Also note that this line:

( mkdir -p dir; cd dir; pwd; )

Will also not change the current directory in current shell because commands inside square brackets execute in a sub shell whereas curly braces are just used for grouping.

The behavior of pipeline commands regarding variables is implementation defined.

You happen to use bash which choose to put every component in a background shell so the cd effect is lost in the main shell.

Should you have run ksh which choose to keep the last element of a pipeline in the current shell and should you have put the cd in the last statement, the behavior would have been the one you expect.

$ bash
$ { mkdir -p dir; cd dir; pwd; } | { cat; mkdir -p dir; cd dir; pwd ; } ; pwd;
/tmp/dir
/tmp/dir
/tmp
$ ksh
$ { mkdir -p dir; cd dir; pwd; } | { cat; mkdir -p dir; cd dir; pwd ; } ; pwd;
/tmp/dir
/tmp/dir
/tmp/dir

The pipe does not reset the current working directory. The pipe creates subshells, in bash one for each side of the pipe. The subshell also does not reset the current working directory. The subshell contains the changes to the environment in itself and does not propagate them to the parent shell.

In your first script:

$ { mkdir dir; cd dir; pwd; } | cat; pwd;

The cd is executed inside the left subshell of the pipe. The working directory is changed only in that subshell. The working directory of the parent shell is not changed. The first pwd is executed in the same subshell as the cd so it reads the changed working directory. The pwd at the end is executed in the parent shell so it reads the working directory of the parent shell which was not changed.

In your second script:

$ { mkdir dir; cd dir; pwd; }; pwd;

There is no subshell created. The curly braces do not create a subshell. The cd is executed in the parent shell and both pwd s read the changed working directory.

For more information and explanation read the fine bash manual about:

Although the manuals says:

   { list; }

Placing a list of commands between curly braces causes the list to be executed in the current shell context. No subshell is created.

Placing it on a pipeline would still place it on a subshell.

   { list; } | something

Since

Each command in a pipeline is executed in its own subshell (see Command Execution Environment).

The commands in { } itself would not be placed on a subshell but the higher context of it itself would be that's why it would still be the same.

As a test running ( ) and { } would just be the same:

# echo "$BASHPID"
6582
# ( ps -p "$BASHPID" -o ppid= ) | cat
 6582
# { ps -p "$BASHPID" -o ppid=; } | cat
 6582

Both sends parent process as the calling shell.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM