207

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?

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
BassKozz
  • 3,007
  • 6
  • 24
  • 18
  • 1
    Well, as you noticed, if you do # first, then the \ becomes just part of the comment, but if you do \ first, then the later characters on the line change its meaning away from "line continuation" to "quote". I've thought of one solution, given below. – DigitalRoss Sep 21 '09 at 18:27
  • 1
    possible duplicate of [How to Put Line Comment for a Multi-line Command](http://stackoverflow.com/questions/9522631/how-to-put-line-comment-for-a-multi-line-command) – Braiam Jan 25 '15 at 20:16

10 Answers10

252

This will have some overhead, but technically it does answer your question:

echo abc `#Put your comment here` \
     def `#Another chance for a comment` \
     xyz, etc.

And for pipelines specifically, there is a clean solution with no overhead:

echo abc |        # Normal comment OK here
     tr a-z A-Z | # Another normal comment OK here
     sort |       # The pipelines are automatically continued
     uniq         # Final comment

See Stack Overflow question How to Put Line Comment for a Multi-line Command.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
  • 1
    Seems rather complex, if there no simpler method? – BassKozz Sep 21 '09 at 18:23
  • 1
    Ok, I've added a slightly simpler variation. – DigitalRoss Sep 21 '09 at 18:31
  • 1
    Can you modify your answer just to show the fact that the backslash is not needed so I can put the comments next to each line and just use a pipe? – BassKozz Sep 21 '09 at 18:47
  • I verified that versions one and two work. However, can you explain why they do and what is going on here? Thanks. – Faheem Mitha Aug 21 '11 at 13:50
  • There used to be a shell `goto` command which branched to labels specified like `: here`. The goto is gone but you can still use the `: whatever` syntax ... `:` is a sort of parsed comment now. – DigitalRoss Aug 21 '11 at 14:49
  • 1
    Thanks for the explanation. I've opened a question on unix.sx asking for more details, [bash multi line command with comments after the continuation character](http://unix.stackexchange.com/questions/19124/bash-multi-line-command-with-comments-after-the-continuation-character). – Faheem Mitha Aug 21 '11 at 15:18
  • The "for pipelines specifically" method is superficially nice, but it has a very annoying drawback: if I copy-paste your example into an interactive bash shell, it works great... but then if I then type !! or up-arrow to re-execute the command, it gets mangled. Your first suggestion seems to be more robust. – Don Hatch Oct 14 '16 at 09:39
  • fyi, this is strangely not compatible with `zsh`, which is more commonly the default shell for Macs... The colon (`\`: comment\``) version works with `zsh` though. So if you want something more compatible, that one looks like a better option... – Garrett Motzner Oct 21 '22 at 20:10
45

The trailing backslash must be the last character on the line for it to be interpreted as a continuation command. No comments or even whitespace are allowed after it.

You should be able to put comment lines in between your commands

# output MYSQLDUMP file
cat ${MYSQLDUMP} | \
# simplify the line
sed '/created_at/d' | \
# create some newlines
tr ",;" "\n" | \
# use some sed magic
sed -e 's/[asbi]:[0-9]*[:]*//g' -e '/^[{}]/d' -e 's/""//g' -e '/^"{/d' | \
# more magic
sed -n -e '/^"/p' -e '/^print_value$/,/^option_id$/p' | \
# even more magic
sed -e '/^option_id/d' -e '/^print_value/d' -e 's/^"\(.*\)"$/\1/' | \
tr "\n" "," | \
# I hate phone numbers in my output
sed -e 's/,\([0-9]*-[0-9]*-[0-9]*\)/\n\1/g' -e 's/,$//' | \ 
# one more sed call and then send it to the CSV file
sed -e 's/^/"/g' -e 's/$/"/g' -e 's/,/","/g' >> ${CSV}
mob
  • 117,087
  • 18
  • 149
  • 283
  • 16
    The \ is not necessary when the pipeline command component ends with | – DigitalRoss Sep 21 '09 at 18:36
  • 2
    DigitalRoss, You are correct, I can just use the pipe and not the backslash and then my #comments will work perfectly... can you post that as an answer so I can accept it. – BassKozz Sep 21 '09 at 18:46
  • 11
    "You should be able to put comment lines in between your commands": no, this is only working because the last interpreted character of the previous lines is `|`. If you try `cat file1\#commentfile2`, you'll see you don't get `cat file1 file2`, but rather `cat file1; file2`. – dubiousjim Nov 14 '12 at 19:32
  • 6
    However, as others have mentioned, `cat file1 | # commentsort` works fine. So too does `cat file1 && # commentecho foo`. So comments can be included after `|` or `&&` or `||`, but not after `\\` or in the middle of a command. – dubiousjim Jan 12 '13 at 22:53
9

As DigitalRoss pointed out, the trailing backslash is not necessary when the line woud end in |. And you can put comments on a line following a |:

 cat ${MYSQLDUMP} |         # Output MYSQLDUMP file
 sed '1d' |                 # skip the top line
 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/,$//' |   # hate phone numbers
 sed -e 's/^/"/g' -e 's/$/"/g' -e 's/,/","/g' >> ${CSV}
mob
  • 117,087
  • 18
  • 149
  • 283
6

$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.

Tom Hale
  • 40,825
  • 36
  • 187
  • 242
6

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 #.

wisbucky
  • 33,218
  • 10
  • 150
  • 101
5

The backslash escapes the #, interpreting it as its literal character instead of a comment character.

tobiasvl
  • 570
  • 4
  • 20
2

Here is a bash script that combines the ideas and idioms of several previous comments to provide, with examples, inline comments having the general form ${__:+ <comment text>}.

In particular

  • <comment text> can be multi-line
  • <comment text> is not parameter-expanded
  • no subprocesses are spawned (so comments are efficient)

There is one restriction on the <comment text>, namely, unbalanced braces '}' and parentheses ')' must be protected (i.e., '\}' and '\)').

There is one requirement on the local bash environment:

  • the parameter name __ must be unset

Any other syntactically valid bash parameter-name will serve in place of __, provided that the name has no set value.

An example script follows

# provide bash inline comments having the form
#     <code> ${__:+ <comment>} <code> 
#     <code> ${__:+ <multiline
#                   comment>} <code>

# utility routines that obviate "useless use of cat"
function bashcat { printf '%s\n' "$(</dev/stdin)"; }
function scat { 1>&2 bashcat; exit 1; }

# ensure that '__' is unset && remains unset
[[ -z ${__+x} ]] &&  # if '__' is unset
  declare -r __ ||   # then ensure that '__' remains unset 
  scat <<EOF         # else exit with an error
Error: the parameter __='${__}' is set, hence the
  comment-idiom '\${__:+ <comment text>}' will fail
EOF

${__:+ (example of inline comments)
------------------------------------------------
the following inline comment-idiom is supported
    <code> ${__:+ <comment>} <code> 
    <code> ${__:+ <multiline
                   comment>} <code> 
(advisory) the parameter '__' must NOT be set;
  even the null declaration __='' will fail
(advisory) protect unbalanced delimiters \} and \) 
(advisory) NO parameter-expansion of <comment> 
(advisory) NO subprocesses are spawned
(advisory) a functionally equivalent idiom is 
    <code> `# <comment>` <code> 
    <code> `# <multiline
               comment>` <code>
however each comment spawns a bash subprocess
that inelegantly requires ~1ms of computation 
------------------------------------------------}
Mehrad Mahmoudian
  • 3,466
  • 32
  • 36
John Sidles
  • 159
  • 4
  • Just for transparency as my changes are not peer-reviewed: The original post has `${__+` and it produced some error for me and after some searching I realized it should comply with `${name:+word}` format to work and hence I changed it to `${__:+`. For more info check out https://zsh.sourceforge.io/Doc/Release/Expansion.html and https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html – Mehrad Mahmoudian Oct 05 '21 at 14:54
2

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.

Jim Grisham
  • 384
  • 3
  • 11
1

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.

Glyph
  • 31,152
  • 11
  • 87
  • 129
0

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
  } \
| ...
xebeche
  • 905
  • 7
  • 20
  • 1
    Doesn't that work even removing your list elements? – Jim Grisham May 02 '21 at 00:22
  • 1
    @JimGrisham Yes, thanks for pointing this out. I've changed my answer accordingly. Now, technically, it's identical to other answers but strangely, so far, nobody else had shown code with leading pipes. – xebeche May 02 '21 at 21:38