Commenting in a Bash script inside a multiline command

Viewed 329112

How can I comment on each line of the following lines from a script?

cat ${MYSQLDUMP} | \
sed '1d' | \
tr ",;" "\n" | \
sed -e 's/[asbi]:[0-9]*[:]*//g' -e '/^[{}]/d' -e 's/""//g' -e '/^"{/d' | \
sed -n -e '/^"/p' -e '/^print_value$/,/^option_id$/p' | \
sed -e '/^option_id/d' -e '/^print_value/d' -e 's/^"\(.*\)"$/\1/' | \
tr "\n" "," | \
sed -e 's/,\([0-9]*-[0-9]*-[0-9]*\)/\n\1/g' -e 's/,$//' | \
sed -e 's/^/"/g' -e 's/$/"/g' -e 's/,/","/g' >> ${CSV}

If I try and add a comment like:

cat ${MYSQLDUMP} | \ # Output MYSQLDUMP File

I get:

#: not found

Is it possible to comment here?

10 Answers

$IFS comment hacks

This hack uses parameter expansion on $IFS, which is used to separate words in commands:

$ echo foo${IFS}bar
foo bar

Similarly:

$ echo foo${IFS#comment}bar
foo bar

Using this, you can put a comment on a command line with contination:

$ echo foo${IFS# Comment here} \
> bar
foo bar

but the comment will need to be before the \ continuation.

Note that parameter expansion is performed inside the comment:

$ ls file
ls: cannot access 'file': No such file or directory
$ echo foo${IFS# This command will create file: $(touch file)}bar
foo bar
$ ls file
file

Rare exception

The only rare case this fails is if $IFS previously started with the exact text which is removed via the expansion (ie, after the # character):

$ IFS=x
$ echo foo${IFS#y}bar
foo bar
$ echo foo${IFS#x}bar
foobar

Note the final foobar has no space, illustrating the issue.

Since $IFS contains only whitespace by default, it's extremely unlikely you'll run into this problem.


Credit to @pjh's comment which sparked off this answer.

In addition to the examples by DigitalRoss, here's another form that you can use if you prefer $() instead of backticks `

echo abc $(: comment) \
     def $(: comment) \
     xyz

Of course, you can use the colon syntax with backticks as well:

echo abc `: comment` \
     def `: comment` \
     xyz

Additional Notes

The reason $(#comment) doesn't work is because once it sees the #, it treats the rest of the line as comments, including the closing parentheses: comment). So the parentheses is never closed.

Backticks parse differently and will detect the closing backtick even after a #.

Instead of what you tried:

cat ${MYSQLDUMP} | \ # Output MYSQLDUMP File

Others have mentioned that this should work:

cat ${MYSQLDUMP} |   # Output MYSQLDUMP File

Since split lines won't always end in a pipe (|), though, you can put comments on their own line, like this:

date && \
  # List current directory
  ls -l | awk '{ \
  # Filename is in the ninth column
      # This is just making "ls -l" work mostly like "ls -1"
  print $9 }'

Just don't do so in the middle of a string:

echo " Hello \
 # Localized name for your planet:
world."

In your case, you can use this method:

cat ${MYSQLDUMP} | \
# Output MYSQLDUMP File

Extended example:

# Create .csv file from MySQL dump file
cat ${MYSQLDUMP} |   
# Output MYSQLDUMP File
# and pipe to first sed command
sed '1d' | \
# Pipe output to tr
tr ",;" "\n" | \
# Apply sed expression
sed -e 's/[asbi]:[0-9]*[:]*//g' -e '/^[{}]/d' -e 's/""//g' -e '/^"{/d' | \
    # Apply another two sed expressions
    # (and since whitespace is ignored, you can intent for clarity)
    sed -n -e '/^"/p' -e '/^print_value$/,/^option_id$/p' | \
    # Apply three more sed expressions
    sed -e '/^option_id/d' -e '/^print_value/d' -e 's/^"\(.*\)"$/\1/' | \
    # Use tr to ...
    tr "\n" "," | \
    # Apply yet another two sed expressions
    sed -e 's/,\([0-9]*-[0-9]*-[0-9]*\)/\n\1/g' -e 's/,$//' | \
    # Apply the final three sed expressions
    sed -e 's/^/"/g' -e 's/$/"/g' -e 's/,/","/g' >> ${CSV}

... or mix both methods:

# Create .csv file from MySQL dump file
cat ${MYSQLDUMP} |   # Output MYSQLDUMP File
# and pipe to first sed command
sed '1d' | \
# Pipe output to tr
...

(I believe both methods work since shell script files are parsed line-by-line, as is CLI input.)

Final notes:

  • It is important to remember that the line continuation character (\), when used, should be the last character in that line (even a single forgotten trailing space can ruin your evening).

  • If typing manually from the command line, use only the second method (with each comment on its own line) if you intend on using the command history feature.

  • If using history and want comments preserved, do not use either of these methods - use one from a different answer to this question.

My preferred coding style for pipe connected commands is

command1 \
| command2 \
| ...

As @JimGrisham and others suggested, one way to comment lines is

command1 \
| # inline comment
  command2 \
| ...

Another way that does not invoke subshells is using Bash's { list; } construct which works always. So here:

command1 \
| {
    # inline comment
    command2
  } \
| ...

The answer here is contextual. You want to be able to annotate long lines of parameters and also long complex pipelines, and sadly the syntax does not work the same way.

But there is a 3-step process that can let you comment almost anything in detail, without the subshell overhead of `#...` or the very complicated quoting land-mines of ${IFS#...}:

Put everything into a function so you can use local variables

You're writing a complex enough shell script to have functions anyway, right? We need to give everything names in order to use the named-array syntax, but we also don't want to pollute the global shell namespace, so tidy them away into local variable declarations.

Put long commands into arrays

Arrays don't need \ line continuations, so you can use comments as normal, like this:

local x=(
    "command-name"
    # here's the command
    "argument" # and here's the first arg
)

Put pipelines after all arrays have been defined

If a redirection is complex, you may want to explain it, but if you have a long list of parameters it might get in your way. So use a couple of short-named local vars that will be short enough to fit your comments, then use redirections or pipelines to break up the lines, not backslashes:

"${x[@]} | # here is my first command
    "${y[@]} | # and here's the second one
    ...

That way the redirection can never get so long that your comments won't fit; you get to choose those variable names, and because they're local you don't need to make them unique for your whole script.

Put it all together

When you get it all together, it looks like this:

f() {
    local produce_text=(
        echo
        # I'm echoing
        "first   argument"
        # newline!
        '
'
        # Here's the first arg.
        "second    argument"
        # Here's the second.
    )
    local number_lines=(
        cat
        # I'm concatenating
        -b
        # with numbered lines
    )
    "${produce_text[@]}" |
        # explaining the pipeline
        "${number_lines[@]}" \
            2>&1
    # done
}

N.B.: Everything in this answer was also checked with zsh, so while it (probably?) won't work in POSIX sh, it's not entirely bash-specific.

Related