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
Noil Noil
  • 131
  • 4
    that's bizarre. – glenn jackman Jan 22 '16 at 20:04
  • 2
    If I use echo instead of ls, it works the second way in both the cases. – choroba Jan 22 '16 at 20:11
  • 1
    Looks somewhat similar to the code example in this answer. – Wildcard Jan 22 '16 at 20:13
  • 4
    /bin/echo preserves the difference, so it seems like output redirections for external commands happen in a subshell. – chepner Jan 22 '16 at 20:14
  • 2
    Definitely worth a bug report to bug-bash@gnu.org; it's not fixed in 4.4, currently under development. – chepner Jan 22 '16 at 20:17
  • 1
    Are you sure you're running bash? I tried it in bash (4.3), it works as expected (echo $i in the end returns 6). Though in sh it works exactly as in your example. – rush Jan 22 '16 at 20:24
  • what if you remove the spaces between (( and++i and )). maybe its nothing, but bash's splitting on <> redirections is always screwy and non-standard anyway. – mikeserv Jan 22 '16 at 23:22
  • 1
    @rush Sure you didn't change anything? I'm using bash 4.3.30 on debian 8.2 and I'm getting the same results as Noil Noil in the question. I can also confirm choroba's findings using the bash builtin echo command. However, explicitly using /bin/echo yields the same result as described in the original question. So that seems to support the explaination offered by chepner. – Shevek Jan 22 '16 at 23:29
  • It's bash: 3.2.51(1)-release (x86_64-suse-linux-gnu) – Noil Noil Jan 23 '16 at 02:39
  • 1
    @mikeserv - removing spaces doesn't change anything and it's not a bash-only thing - I get the same behavior with zsh 5.2 – don_crissti Jan 23 '16 at 02:57
  • @don_crissti - zsh doesnt expand the spaces to separate fields like bash. I think bash is the only shell in which you might have to quote a redirection target word against ifs splitting. So I was curious if it mattered. I didn't think it would. – mikeserv Jan 23 '16 at 03:18
  • @Wildcard The problem with dash in this case is that it doesn't understand i++, nor ++i, but it process ++i as a case of double +i. In dash, the file "written to" will be 5 as well as the value printed. Instead, zsh and bash will "write to" file 6 and print 5. That proves that ++i was interpreted as an increment but not kept in memory. –  Jan 23 '16 at 04:25
  • @BinaryZebra, I'm pretty good with shell scripting, but the only conclusion I take away from this discussion is: Never use increment operators within arithmetic substitution. The variety of possible results is too confusing. – Wildcard Jan 23 '16 at 04:29

1 Answers1

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)

  • 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. – Murphy Feb 20 '16 at 09:57