{"id":865,"date":"2020-04-05T23:29:25","date_gmt":"2020-04-05T23:29:25","guid":{"rendered":"http:\/\/www.vidarholen.net\/contents\/blog\/?p=865"},"modified":"2020-04-05T23:33:28","modified_gmt":"2020-04-05T23:33:28","slug":"the-curious-pitfalls-of-shell-redirections-to-i","status":"publish","type":"post","link":"https:\/\/www.vidarholen.net\/contents\/blog\/?p=865","title":{"rendered":"The curious pitfalls in shell redirections to $((i++))"},"content":{"rendered":"\n<div class=\"wp-block-jetpack-markdown\"><p>ShellCheck v0.7.1 has <a href=\"https:\/\/github.com\/koalaman\/shellcheck\/releases\/tag\/v0.7.1\">just been released<\/a>. It primarily has cleanups and bugfixes for existing checks, but also some new ones. The new check I personally find the most fascinating is this one, for an issue I haven&#8217;t really seen discussed anywhere before:<\/p>\n<pre><code>In demo line 6:\n  cat template\/header.txt &quot;$f&quot; &gt; archive\/$((i++)).txt\n                                             ^\n  SC2257: Arithmetic modifications in command redirections\n          may be discarded. Do them separately.\n<\/code><\/pre>\n<p>Here&#8217;s the script in full:<\/p>\n<pre><code class=\"language-sh\">#!\/bin\/bash\ni=1\nfor f in *.txt\ndo\n  echo &quot;Archiving $f as $i.txt&quot;\n  cat template\/header.txt &quot;$f&quot; &gt; archive\/$((i++)).txt\ndone\n<\/code><\/pre>\n<p>Seasoned shell scripter may already have jumped ahead, tried it in their shell, and found that the change is not discarded, at least not in their Bash 5.0.16(1):<\/p>\n<pre><code>bash-5.0$ i=0; echo foo &gt; $((i++)).txt; echo &quot;$i&quot; \n1\n<\/code><\/pre>\n<p>Based on this, you may be expecting a quick look through the Bash commit history, and maybe a plea that we should be kind to our destitute brethren on macOS with Bash 3.<\/p>\n<p>But no. Here&#8217;s the demo script on the same system:<\/p>\n<pre><code>bash-5.0$ .\/demo\nArchiving chocolate_cake_recipe.txt as 1.txt\nArchiving emo_poems.txt as 1.txt\nArchiving project_ideas.txt as 1.txt\n<\/code><\/pre>\n<p>The same is true for <code>source .\/demo<\/code>, which runs the script in the exact same shell instance that we just tested on. Furthermore, it only happens in redirections, and not in arguments.<\/p>\n<p>So what&#8217;s going on?<\/p>\n<p>As it turns out, Bash, Ksh and BusyBox ash all expand the redirection filename as part of setting up file descriptors. If you are familiar with the Unix process model, the pseudocode would be something like this:<\/p>\n<pre><code class=\"language-c\">if command is external:\n  fork child process:\n    filename := expandString(command.stdout) # Increments i\n    fd[1] := open(filename)\n    execve(command.executable, command.args)\nelse:\n  filename := expandString(command.stdout)   # Increments i\n  tmpFd := open(filename)\n  run_internal_command(command, stdout=tmpFD)\n  close(tmpFD)\n<\/code><\/pre>\n<p>In other words, the scope of the variable modification depends on whether the shell forked off a new process in anticipation of executing the command.<\/p>\n<p>For shell builtin commands that don&#8217;t or can&#8217;t fork, like <code>echo<\/code>, this means that the change takes effect in the current shell. This is the test we did.<\/p>\n<p>For external commands, like <code>cat<\/code>, the change is only visible between the time the file descriptor is set up until the command is invoked to take over the process. This is what the demo script does.<\/p>\n<p>Of course, subshells are well known to experienced scripters, and also described on this blog in the article <a href=\"https:\/\/www.vidarholen.net\/contents\/blog\/?p=178\">Why Bash is like that: Subshells<\/a>, but to me, this is a new and especially tricky source of them.<\/p>\n<p>For example, the script works fine in <code>busybox sh<\/code>, where <code>cat<\/code> is a builtin:<\/p>\n<pre><code>$ busybox sh demo\nArchiving chocolate_cake_recipe.txt as 1.txt\nArchiving emo_poems.txt as 2.txt\nArchiving project_ideas.txt as 3.txt\n<\/code><\/pre>\n<p>Similarly, the scope may depend on whether you overrode any commands with a wrapper function:<\/p>\n<pre><code>awk() { gawk &quot;$@&quot;; }\n# Increments\nawk 'BEGIN {print &quot;hi&quot;; exit;}' &gt; $((i++)).txt\n# Does not increment\ngawk 'BEGIN {print &quot;hi&quot;; exit;}' &gt; $((i++)).txt  \n<\/code><\/pre>\n<p>Or if you want to override an alias, the result depends on whether you used <code>command<\/code> or a <a href=\"https:\/\/www.vidarholen.net\/contents\/blog\/?p=365\">leading backslash<\/a>:<\/p>\n<pre><code># Increments\ncommand git show . &gt; $((i++)).txt\n# Does not increment\n\\git show . &gt; $((i++)).txt\n<\/code><\/pre>\n<p>To avoid this confusion, consider following <a href=\"https:\/\/github.com\/koalaman\/shellcheck\/wiki\/SC2257\">ShellCheck&#8217;s advice<\/a> and just increment the variable separately if it&#8217;s part of the filename in a redirection:<\/p>\n<pre><code>anything &gt; &quot;$((i++)).txt&quot;\n: $((i++))\n<\/code><\/pre>\n<p>Thanks to Strolls on #bash@Freenode for pointing out this behavior.<\/p>\n<p>PS: While researching this article, I found that <code>dash<\/code> always increments (though with <code>$((i=i+1))<\/code> since it doesn&#8217;t support <code>++<\/code>). ShellCheck v0.7.1 still warns, but git master does not.<\/p>\n<\/div>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true},"categories":[5,4],"tags":[11,46,39],"class_list":["post-865","post","type-post","status-publish","format-standard","hentry","category-advanced-linux","category-linux","tag-bash","tag-shellcheck","tag-why-bash-is-like-that"],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=\/wp\/v2\/posts\/865","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=865"}],"version-history":[{"count":11,"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=\/wp\/v2\/posts\/865\/revisions"}],"predecessor-version":[{"id":876,"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=\/wp\/v2\/posts\/865\/revisions\/876"}],"wp:attachment":[{"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=865"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=865"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.vidarholen.net\/contents\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=865"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}