13

The following line creates file_c-6.txt but outputs 5:

$ i=5; ls file_a-${i}.txt file_b-${i}.txt > file_c-$(( ++i )).txt; echo $i 5 $ cat file_c-6.txt file_a-5.txt file_b-5.txt 

If one removes > it would list file_c-6.txt and output 5:

I can't understand why it doesn't keep the value of i in the first example.

$ i=5; ls file_a-${i}.txt file_b-${i}.txt file_c-$(( ++i )).txt; echo $i file_a-5.txt file_b-5.txt file_c-6.txt 6 
13
  • 4
    that's bizarre. Commented Jan 22, 2016 at 20:04
  • 2
    If I use echo instead of ls, it works the second way in both the cases. Commented Jan 22, 2016 at 20:11
  • 1
    Looks somewhat similar to the code example in this answer. Commented Jan 22, 2016 at 20:13
  • 4
    /bin/echo preserves the difference, so it seems like output redirections for external commands happen in a subshell. Commented Jan 22, 2016 at 20:14
  • 2
    Definitely worth a bug report to [email protected]; it's not fixed in 4.4, currently under development. Commented Jan 22, 2016 at 20:17

1 Answer 1

1

If you run this under strace, you can see that the version that uses ls starts up the command in a subshell, where the version which uses echo executes it all in the existing shell.

Compare the output of

$ strace -f /bin/bash -o trace.txt -c 'i=5; echo $i; echo file_c-$((++i)).txt; echo $i' 5 6 6 

against

strace -f /bin/bash -o trace.txt -c 'i=5; echo $i; ls > file_c-$((++i)).txt; echo $i' 5 5 

You'll see in the first:

1251 execve("/bin/bash", ["/bin/bash", "-c", "i=5; echo $i; echo file_c-$(( ++"...], [/* 19 vars */]) = 0 ... 1251 write(1, "5\n", 2) = 2 1251 write(1, "file_c-6.txt\n", 13) = 13 1251 write(1, "6\n", 2) = 2 

And in the second:

1258 execve("/bin/bash", ["/bin/bash", "-c", "i=5; echo $i; ls > file_c-$(( ++"...], [/* 19 vars */]) = 0 ... 1258 write(1, "5\n", 2) = 2 ... 1258 stat("/bin/ls", {st_mode=S_IFREG|0755, st_size=110080, ...}) = 0 1258 access("/bin/ls", R_OK) = 0 1258 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7301f40a10) = 1259 1259 open("file_c-6.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 1259 dup2(3, 1) = 1 1259 close(3) = 0 1259 execve("/bin/ls", ["ls"], [/* 19 vars */]) = 0 1259 write(1, "71\nbin\nfile_a-5.txt\nfile_b-5.txt"..., 110) = 110 1259 close(1) = 0 1259 munmap(0x7f0e81c56000, 4096) = 0 1259 close(2) = 0 1259 exit_group(0) = ? 1259 +++ exited with 0 +++ 1258 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 1259 1258 rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f7301570d40}, {0x4438a0, [], SA_RESTORER, 0x7f7301570d40}, 8) = 0 1258 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0 1258 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1259, si_status=0, si_utime=0, si_stime=0} --- 1258 wait4(-1, 0x7ffd23d86e98, WNOHANG, NULL) = -1 ECHILD (No child processes) 1258 rt_sigreturn() = 0 1258 write(1, "5\n", 2) = 2 

In this last example, you see the clone into a new process (from 1258 -> 1259), so now we're in a subprocess. The opening of file_c-6.txt which means that we've evaluated $((++i)) in the subshell, and the execution of ls with its stdout set to that file.

Finally, we see that the subprocess exits, we reap the child, then we continue with where we left off... with $i set to 5, and that's what we echo out again.

(Remember variable changes in a subprocess do not percolate up to the parent process, unless you do something explicitly in the parent to grab the child's changes)

1
  • Excellent analysis. One solution would be to use a temporary variable for the incremented value: i=5; j=$(( i + 1 )); ls file_a-${i}.txt file_b-${i}.txt > file_c-${j}.txt; i=${j}; echo $i. Commented Feb 20, 2016 at 9:57

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.