mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 1 | <?xml version="1.0"?> |
| 2 | <?xml-stylesheet type="text/xsl" href="styleguide.xsl"?> |
| 3 | <GUIDE title="Shell Style Guide"> |
| 4 | |
| 5 | <p align="right"> |
| 6 | |
mark@chromium.org | 7b24563 | 2013-09-25 21:16:00 +0000 | [diff] [blame] | 7 | Revision 1.26 |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 8 | </p> |
| 9 | |
| 10 | |
| 11 | <address> |
| 12 | Paul Armstrong<br/> |
| 13 | Too many more to mention<br/> |
| 14 | </address> |
| 15 | |
| 16 | <OVERVIEW> |
| 17 | |
| 18 | <CATEGORY title="Background"> |
| 19 | |
Alexander F Rødseth | 7ebdcc4 | 2016-05-19 23:04:17 +0200 | [diff] [blame] | 20 | |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 21 | |
| 22 | <STYLEPOINT title="Which Shell to Use"> |
| 23 | <SUMMARY> |
| 24 | <code>Bash</code> is the only shell scripting language permitted for |
| 25 | executables. |
| 26 | </SUMMARY> |
| 27 | <BODY> |
| 28 | <p> |
| 29 | Executables must start with <code>#!/bin/bash</code> and a minimum |
| 30 | number of flags. Use <code>set</code> to set shell options so that |
| 31 | calling your script as <code>bash <i><script_name></i></code> |
| 32 | does not break its functionality. |
| 33 | </p> |
| 34 | <p> |
| 35 | Restricting all executable shell scripts to <b>bash</b> |
| 36 | gives us a consistent shell language that's installed on all our |
| 37 | machines. |
| 38 | </p> |
| 39 | <p> |
| 40 | The only exception to this is where you're forced to by whatever |
| 41 | you're coding for. One example of this is Solaris SVR4 packages which |
| 42 | require plain Bourne shell for any scripts. |
| 43 | </p> |
| 44 | </BODY> |
| 45 | </STYLEPOINT> |
| 46 | |
| 47 | <STYLEPOINT title="When to use Shell"> |
| 48 | <SUMMARY> |
| 49 | Shell should only be used for small utilities or simple wrapper |
| 50 | scripts. |
| 51 | </SUMMARY> |
| 52 | <BODY> |
| 53 | <p> |
| 54 | While shell scripting isn't a development language, it is used for |
| 55 | writing various utility scripts throughout Google. This |
| 56 | style guide is more a recognition of its use rather than |
| 57 | a suggestion that it be used for widespread deployment. |
| 58 | </p> |
| 59 | <p> |
| 60 | Some guidelines: |
| 61 | <ul> |
| 62 | <li> |
| 63 | If you're mostly calling other utilities and are doing relatively |
| 64 | little data manipulation, shell is an acceptable choice for the |
| 65 | task. |
| 66 | </li> |
| 67 | <li> |
| 68 | If performance matters, use something other than shell. |
| 69 | </li> |
| 70 | <li> |
| 71 | If you find you need to use arrays for anything more than |
| 72 | assignment of <code>${PIPESTATUS}</code>, you should use Python. |
| 73 | </li> |
| 74 | <li> |
| 75 | If you are writing a script that is more than 100 lines long, you |
| 76 | should probably be writing it in Python instead. Bear in mind |
| 77 | that scripts grow. Rewrite your script in another language |
| 78 | early to avoid a time-consuming rewrite at a later date. |
| 79 | </li> |
| 80 | </ul> |
| 81 | </p> |
| 82 | </BODY> |
| 83 | </STYLEPOINT> |
| 84 | </CATEGORY> |
| 85 | |
| 86 | </OVERVIEW> |
| 87 | |
| 88 | <CATEGORY title="Shell Files and Interpreter Invocation"> |
| 89 | |
| 90 | <STYLEPOINT title="File Extensions"> |
| 91 | <SUMMARY> |
| 92 | Executables should have no extension (strongly preferred) or a |
| 93 | <code>.sh</code> extension. |
| 94 | Libraries must have a <code>.sh</code> extension and should not be |
| 95 | executable. |
| 96 | </SUMMARY> |
| 97 | <BODY> |
| 98 | <p> |
| 99 | It is not necessary to know what language a program is written in when |
| 100 | executing it and shell doesn't require an extension so we prefer not to |
| 101 | use one for executables. |
| 102 | </p> |
| 103 | <p> |
| 104 | However, for libraries it's important to know what language it is and |
| 105 | sometimes there's a need to have similar libraries in different |
| 106 | languages. This allows library files with identical purposes but |
| 107 | different languages to be identically named except for the |
| 108 | language-specific suffix. |
| 109 | </p> |
| 110 | </BODY> |
| 111 | </STYLEPOINT> |
| 112 | |
| 113 | <STYLEPOINT title="SUID/SGID"> |
| 114 | <SUMMARY> |
| 115 | SUID and SGID are <em>forbidden</em> on shell scripts. |
| 116 | </SUMMARY> |
| 117 | <BODY> |
| 118 | <p> |
| 119 | There are too many security issues with shell that make it nearly |
| 120 | impossible to secure sufficiently to allow SUID/SGID. While bash does |
| 121 | make it difficult to run SUID, it's still possible on some platforms |
| 122 | which is why we're being explicit about banning it. |
| 123 | </p> |
| 124 | <p> |
| 125 | Use <code>sudo</code> to provide elevated access if you need it. |
| 126 | </p> |
| 127 | </BODY> |
| 128 | </STYLEPOINT> |
| 129 | |
| 130 | </CATEGORY> |
| 131 | |
| 132 | <CATEGORY title="Environment"> |
| 133 | |
| 134 | <STYLEPOINT title="STDOUT vs STDERR"> |
| 135 | <SUMMARY> |
| 136 | All error messages should go to <code>STDERR</code>. |
| 137 | </SUMMARY> |
| 138 | <BODY> |
| 139 | <p> |
| 140 | This makes it easier |
| 141 | to separate normal status from actual issues. |
| 142 | </p> |
| 143 | <p> |
| 144 | A function to print out error messages along with other status |
| 145 | information is recommended. |
| 146 | <CODE_SNIPPET> |
| 147 | err() { |
| 148 | echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2 |
| 149 | } |
| 150 | |
| 151 | if ! do_something; then |
| 152 | err "Unable to do_something" |
| 153 | exit "${E_DID_NOTHING}" |
| 154 | fi |
| 155 | </CODE_SNIPPET> |
| 156 | </p> |
| 157 | </BODY> |
| 158 | </STYLEPOINT> |
| 159 | |
| 160 | </CATEGORY> |
| 161 | |
| 162 | <CATEGORY title="Comments"> |
| 163 | |
| 164 | <STYLEPOINT title="File Header"> |
| 165 | <SUMMARY> |
| 166 | Start each file with a description of its contents. |
| 167 | </SUMMARY> |
| 168 | <BODY> |
| 169 | <p> |
| 170 | Every file must have a top-level comment including a brief overview of |
| 171 | its contents. |
Alexander F Rødseth | 7ebdcc4 | 2016-05-19 23:04:17 +0200 | [diff] [blame] | 172 | A |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 173 | copyright notice |
| 174 | and author information are optional. |
| 175 | </p> |
| 176 | <p> |
| 177 | Example: |
| 178 | <CODE_SNIPPET> |
| 179 | #!/bin/bash |
| 180 | # |
| 181 | # Perform hot backups of Oracle databases. |
| 182 | </CODE_SNIPPET> |
| 183 | </p> |
| 184 | |
Alexander F Rødseth | 7ebdcc4 | 2016-05-19 23:04:17 +0200 | [diff] [blame] | 185 | |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 186 | </BODY> |
| 187 | </STYLEPOINT> |
| 188 | |
| 189 | <STYLEPOINT title="Function Comments"> |
| 190 | <SUMMARY> |
| 191 | Any function that is not both obvious and short must be commented. Any |
| 192 | function in a library must be commented regardless of length or |
| 193 | complexity. |
| 194 | </SUMMARY> |
| 195 | <BODY> |
| 196 | <p> |
| 197 | It should be possible for someone else to learn how to use your |
| 198 | program or to use a function in your library by reading the comments |
| 199 | (and self-help, if provided) without reading the code. |
| 200 | </p> |
| 201 | <p> |
| 202 | All function comments should contain: |
| 203 | <ul> |
| 204 | <li> |
| 205 | Description of the function |
| 206 | </li> |
| 207 | <li> |
| 208 | Global variables used and modified |
| 209 | </li> |
| 210 | <li> |
| 211 | Arguments taken |
| 212 | </li> |
| 213 | <li> |
| 214 | Returned values other than the default exit status of the last |
| 215 | command run |
| 216 | </li> |
| 217 | </ul> |
| 218 | </p> |
| 219 | <p> |
| 220 | Example: |
| 221 | <CODE_SNIPPET> |
| 222 | #!/bin/bash |
| 223 | # |
| 224 | # Perform hot backups of Oracle databases. |
| 225 | |
| 226 | export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin' |
| 227 | |
| 228 | ####################################### |
| 229 | # Cleanup files from the backup dir |
| 230 | # Globals: |
| 231 | # BACKUP_DIR |
| 232 | # ORACLE_SID |
| 233 | # Arguments: |
| 234 | # None |
| 235 | # Returns: |
| 236 | # None |
| 237 | ####################################### |
| 238 | cleanup() { |
| 239 | ... |
| 240 | } |
| 241 | </CODE_SNIPPET> |
| 242 | </p> |
| 243 | </BODY> |
| 244 | </STYLEPOINT> |
| 245 | |
| 246 | <STYLEPOINT title="Implementation Comments"> |
| 247 | <SUMMARY> |
| 248 | Comment tricky, non-obvious, interesting or important parts of your code. |
| 249 | </SUMMARY> |
| 250 | <BODY> |
| 251 | <p> |
| 252 | This follows general Google coding comment practice. Don't comment |
| 253 | everything. If there's a complex algorithm or you're doing something |
| 254 | out of the ordinary, put a short comment in. |
| 255 | </p> |
| 256 | </BODY> |
| 257 | </STYLEPOINT> |
| 258 | |
| 259 | <STYLEPOINT title="TODO Comments"> |
| 260 | <SUMMARY> |
| 261 | Use TODO comments for code that is temporary, a short-term solution, or |
| 262 | good-enough but not perfect. |
| 263 | </SUMMARY> |
| 264 | <BODY> |
| 265 | <p> |
Philip Kirkbride | e3d0f56 | 2017-05-26 15:45:00 -0400 | [diff] [blame] | 266 | This matches the convention in the <a href="cppguide.html?showone=TODO_Comments#TODO_Comments">C++ |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 267 | Guide</a>. |
| 268 | </p> |
| 269 | <p> |
| 270 | TODOs should include the string TODO in all caps, followed by your |
| 271 | username in parentheses. A colon is optional. It's preferable to put a |
| 272 | bug/ticket number next to the TODO item as well. |
| 273 | </p> |
| 274 | <p> |
| 275 | Examples: |
Alexander F Rødseth | 7ebdcc4 | 2016-05-19 23:04:17 +0200 | [diff] [blame] | 276 | |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 277 | <CODE_SNIPPET> |
| 278 | # TODO(mrmonkey): Handle the unlikely edge cases (bug ####) |
| 279 | </CODE_SNIPPET> |
| 280 | </p> |
| 281 | </BODY> |
| 282 | </STYLEPOINT> |
| 283 | |
| 284 | </CATEGORY> |
| 285 | |
| 286 | <CATEGORY title="Formatting"> |
| 287 | <p> |
| 288 | While you should follow the style that's already there for files that |
| 289 | you're modifying, the following are required for any new code. |
| 290 | </p> |
| 291 | |
| 292 | <STYLEPOINT title="Indentation"> |
| 293 | <SUMMARY> |
| 294 | Indent 2 spaces. No tabs. |
| 295 | </SUMMARY> |
| 296 | <BODY> |
| 297 | <p> |
| 298 | Use blank lines between blocks to improve readability. Indentation is |
| 299 | two spaces. Whatever you do, don't use tabs. For existing files, stay |
| 300 | faithful to the existing indentation. |
| 301 | </p> |
| 302 | </BODY> |
| 303 | </STYLEPOINT> |
| 304 | |
| 305 | <STYLEPOINT title="Line Length and Long Strings"> |
| 306 | <SUMMARY> |
| 307 | Maximum line length is 80 characters. |
| 308 | </SUMMARY> |
| 309 | <BODY> |
| 310 | <p> |
| 311 | If you have to write strings that are longer than 80 characters, this |
| 312 | should be done with a here document or an embedded newline if possible. |
| 313 | Literal strings that have to be longer than 80 chars and can't sensibly |
| 314 | be split are ok, but it's strongly preferred to find a way to make it |
| 315 | shorter. |
| 316 | </p> |
| 317 | <p> |
| 318 | <CODE_SNIPPET> |
| 319 | # DO use 'here document's |
| 320 | cat <<END; |
| 321 | I am an exceptionally long |
| 322 | string. |
| 323 | END |
| 324 | |
| 325 | # Embedded newlines are ok too |
| 326 | long_string="I am an exceptionally |
| 327 | long string." |
| 328 | </CODE_SNIPPET> |
| 329 | </p> |
| 330 | </BODY> |
| 331 | </STYLEPOINT> |
| 332 | |
| 333 | <STYLEPOINT title="Pipelines"> |
| 334 | <SUMMARY> |
| 335 | Pipelines should be split one per line if they don't all fit on one line. |
| 336 | </SUMMARY> |
| 337 | <BODY> |
| 338 | <p> |
| 339 | If a pipeline all fits on one line, it should be on one line. |
| 340 | </p> |
| 341 | <p> |
| 342 | If not, it should be split at one pipe segment per line with the pipe |
| 343 | on the newline and a 2 space indent for the next section of the pipe. |
| 344 | This applies to a chain of commands combined using '|' as well as to |
| 345 | logical compounds using '||' and '&&'. |
| 346 | <CODE_SNIPPET> |
| 347 | # All fits on one line |
| 348 | command1 | command2 |
| 349 | |
| 350 | # Long commands |
| 351 | command1 \ |
| 352 | | command2 \ |
| 353 | | command3 \ |
| 354 | | command4 |
| 355 | </CODE_SNIPPET> |
| 356 | </p> |
| 357 | </BODY> |
| 358 | </STYLEPOINT> |
| 359 | |
| 360 | <STYLEPOINT title="Loops"> |
| 361 | <SUMMARY> |
| 362 | Put <code>; do</code> and <code>; then</code> on the same line as the |
| 363 | <code>while</code>, <code>for</code> or <code>if</code>. |
| 364 | </SUMMARY> |
| 365 | <BODY> |
| 366 | <p> |
| 367 | Loops in shell are a bit different, but we follow the same principles |
| 368 | as with braces when declaring functions. That is: <code>; then</code> |
| 369 | and <code>; do</code> should be on the same line as the if/for/while. |
| 370 | <code>else</code> should be on its own line and closing statements |
| 371 | should be on their own line vertically aligned with the opening |
| 372 | statement. |
| 373 | </p> |
| 374 | <p> |
| 375 | Example: |
| 376 | <CODE_SNIPPET> |
| 377 | for dir in ${dirs_to_cleanup}; do |
| 378 | if [[ -d "${dir}/${ORACLE_SID}" ]]; then |
| 379 | log_date "Cleaning up old files in ${dir}/${ORACLE_SID}" |
| 380 | rm "${dir}/${ORACLE_SID}/"* |
| 381 | if [[ "$?" -ne 0 ]]; then |
| 382 | error_message |
| 383 | fi |
| 384 | else |
| 385 | mkdir -p "${dir}/${ORACLE_SID}" |
| 386 | if [[ "$?" -ne 0 ]]; then |
| 387 | error_message |
| 388 | fi |
| 389 | fi |
| 390 | done |
| 391 | </CODE_SNIPPET> |
| 392 | </p> |
| 393 | </BODY> |
| 394 | </STYLEPOINT> |
| 395 | |
mark@chromium.org | 7b24563 | 2013-09-25 21:16:00 +0000 | [diff] [blame] | 396 | <STYLEPOINT title="Case statement"> |
| 397 | <SUMMARY> |
| 398 | <ul> |
| 399 | <li> |
| 400 | Indent alternatives by 2 spaces. |
| 401 | </li> |
| 402 | <li> |
| 403 | A one-line alternative needs a space after the close parenthesis of |
| 404 | the pattern and before the <code>;;</code>. |
| 405 | </li> |
| 406 | <li> |
| 407 | Long or multi-command alternatives should be split over multiple |
| 408 | lines with the pattern, actions, and <code>;;</code> on separate |
| 409 | lines. |
| 410 | </li> |
| 411 | </ul> |
| 412 | </SUMMARY> |
| 413 | <BODY> |
| 414 | <p> |
| 415 | The matching expressions are indented one level from the 'case' and |
| 416 | 'esac'. Multiline actions are indented another level. In general, |
| 417 | there is no need to quote match expressions. Pattern expressions |
| 418 | should not be preceded by an open parenthesis. Avoid the |
| 419 | <code>;&</code> and <code>;;&</code> notations. |
| 420 | </p> |
| 421 | <CODE_SNIPPET> |
| 422 | case "${expression}" in |
| 423 | a) |
| 424 | variable="..." |
| 425 | some_command "${variable}" "${other_expr}" ... |
| 426 | ;; |
| 427 | absolute) |
| 428 | actions="relative" |
| 429 | another_command "${actions}" "${other_expr}" ... |
| 430 | ;; |
| 431 | *) |
| 432 | error "Unexpected expression '${expression}'" |
| 433 | ;; |
| 434 | esac |
| 435 | </CODE_SNIPPET> |
| 436 | <p> |
| 437 | Simple commands may be put on the same line as the pattern <i>and</i> |
| 438 | <code>;;</code> as long as the expression remains readable. This is |
| 439 | often appropriate for single-letter option processing. When the |
| 440 | actions don't fit on a single line, put the pattern on a line on its |
| 441 | own, then the actions, then <code>;;</code> also on a line of its own. |
| 442 | When on the same line as the actions, use a space after the close |
| 443 | parenthesis of the pattern and another before the <code>;;</code>. |
| 444 | </p> |
| 445 | <CODE_SNIPPET> |
| 446 | verbose='false' |
| 447 | aflag='' |
| 448 | bflag='' |
| 449 | files='' |
| 450 | while getopts 'abf:v' flag; do |
| 451 | case "${flag}" in |
| 452 | a) aflag='true' ;; |
| 453 | b) bflag='true' ;; |
| 454 | f) files="${OPTARG}" ;; |
| 455 | v) verbose='true' ;; |
| 456 | *) error "Unexpected option ${flag}" ;; |
| 457 | esac |
| 458 | done |
| 459 | </CODE_SNIPPET> |
| 460 | </BODY> |
| 461 | </STYLEPOINT> |
| 462 | |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 463 | <STYLEPOINT title="Variable expansion"> |
| 464 | <SUMMARY> |
| 465 | In order of precedence: Stay consistent with what you find; |
| 466 | quote your variables; |
| 467 | prefer "${var}" over "$var", but see details. |
| 468 | </SUMMARY> |
| 469 | <BODY> |
| 470 | <p> |
| 471 | These are meant to be guidelines, as the topic seems too controversial for |
| 472 | a mandatory regulation. |
| 473 | <br/> |
| 474 | They are listed in order of precedence. |
| 475 | </p> |
| 476 | <ol> |
| 477 | <li> |
| 478 | Stay consistent with what you find for existing code. |
| 479 | </li> |
| 480 | <li> |
| 481 | Quote variables, see <a href="#Quoting">Quoting section below</a>. |
| 482 | </li> |
| 483 | <li> |
| 484 | <p> |
| 485 | Don't brace-quote single character shell |
| 486 | specials / positional parameters, unless strictly necessary |
| 487 | or avoiding deep confusion. |
| 488 | <br/> |
| 489 | Prefer brace-quoting all other variables. |
| 490 | <CODE_SNIPPET> |
| 491 | # Section of <em>recommended</em> cases. |
| 492 | |
| 493 | # Preferred style for 'special' variables: |
| 494 | echo "Positional: $1" "$5" "$3" |
| 495 | echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..." |
| 496 | |
| 497 | # Braces necessary: |
| 498 | echo "many parameters: ${10}" |
| 499 | |
| 500 | # Braces avoiding confusion: |
| 501 | # Output is "a0b0c0" |
| 502 | set -- a b c |
| 503 | echo "${1}0${2}0${3}0" |
| 504 | |
| 505 | # Preferred style for other variables: |
| 506 | echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}" |
| 507 | while read f; do |
| 508 | echo "file=${f}" |
| 509 | done < <(ls -l /tmp) |
| 510 | |
| 511 | # Section of <em>discouraged</em> cases |
| 512 | |
| 513 | # Unquoted vars, unbraced vars, brace-quoted single letter |
| 514 | # shell specials. |
| 515 | echo a=$avar "b=$bvar" "PID=${$}" "${1}" |
| 516 | |
| 517 | # Confusing use: this is expanded as "${1}0${2}0${3}0", |
| 518 | # not "${10}${20}${30} |
| 519 | set -- a b c |
| 520 | echo "$10$20$30" |
| 521 | </CODE_SNIPPET> |
| 522 | </p> |
| 523 | </li> |
| 524 | </ol> |
| 525 | </BODY> |
| 526 | </STYLEPOINT> |
| 527 | |
| 528 | <STYLEPOINT title="Quoting"> |
| 529 | <SUMMARY> |
| 530 | <ul> |
| 531 | <li> |
| 532 | Always quote strings containing variables, command substitutions, |
| 533 | spaces or shell meta characters, unless careful unquoted expansion |
| 534 | is required. |
| 535 | </li> |
| 536 | <li> |
| 537 | Prefer quoting strings that are "words" |
| 538 | (as opposed to command options or path names). |
| 539 | </li> |
| 540 | <li> |
| 541 | Never quote <em>literal</em> integers. |
| 542 | </li> |
| 543 | <li> |
| 544 | Be aware of the quoting rules for |
| 545 | <a href="#Test,_%5B_and_%5B%5B">pattern matches in [[</a>. |
| 546 | </li> |
| 547 | <li> |
| 548 | Use "$@" unless you have a specific reason to use $*. |
| 549 | </li> |
| 550 | </ul> |
| 551 | </SUMMARY> |
| 552 | <BODY> |
| 553 | <p> |
| 554 | <CODE_SNIPPET> |
| 555 | # 'Single' quotes indicate that no substitution is desired. |
| 556 | # "Double" quotes indicate that substitution is required/tolerated. |
| 557 | |
| 558 | # Simple examples |
| 559 | # "quote command substitutions" |
| 560 | flag="$(some_command and its args "$@" 'quoted separately')" |
| 561 | |
| 562 | # "quote variables" |
| 563 | echo "${flag}" |
| 564 | |
| 565 | # "never quote literal integers" |
| 566 | value=32 |
| 567 | # "quote command substitutions", even when you expect integers |
| 568 | number="$(generate_number)" |
| 569 | |
| 570 | # "prefer quoting words", not compulsory |
| 571 | readonly USE_INTEGER='true' |
| 572 | |
| 573 | # "quote shell meta characters" |
| 574 | echo 'Hello stranger, and well met. Earn lots of $$$' |
| 575 | echo "Process $$: Done making \$\$\$." |
| 576 | |
| 577 | # "command options or path names" |
| 578 | # ($1 is assumed to contain a value here) |
| 579 | grep -li Hugo /dev/null "$1" |
| 580 | |
| 581 | # Less simple examples |
| 582 | # "quote variables, unless proven false": ccs might be empty |
| 583 | git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"} |
| 584 | |
| 585 | # Positional parameter precautions: $1 might be unset |
| 586 | # Single quotes leave regex as-is. |
| 587 | grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"} |
| 588 | |
| 589 | # For passing on arguments, |
| 590 | # "$@" is right almost everytime, and |
| 591 | # $* is wrong almost everytime: |
| 592 | # |
| 593 | # * $* and $@ will split on spaces, clobbering up arguments |
| 594 | # that contain spaces and dropping empty strings; |
| 595 | # * "$@" will retain arguments as-is, so no args |
| 596 | # provided will result in no args being passed on; |
| 597 | # This is in most cases what you want to use for passing |
| 598 | # on arguments. |
| 599 | # * "$*" expands to one argument, with all args joined |
| 600 | # by (usually) spaces, |
| 601 | # so no args provided will result in one empty string |
| 602 | # being passed on. |
| 603 | # (Consult 'man bash' for the nit-grits ;-) |
| 604 | |
| 605 | set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@") |
| 606 | set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@") |
| 607 | </CODE_SNIPPET> |
| 608 | </p> |
| 609 | </BODY> |
| 610 | </STYLEPOINT> |
| 611 | |
| 612 | </CATEGORY> |
| 613 | |
| 614 | <CATEGORY title="Features and Bugs"> |
| 615 | |
| 616 | <STYLEPOINT title="Command Substitution"> |
| 617 | <SUMMARY> |
| 618 | Use <code>$(command)</code> instead of backticks. |
| 619 | </SUMMARY> |
| 620 | <BODY> |
| 621 | <p> |
| 622 | Nested backticks require escaping the inner ones with <code>\</code>. |
| 623 | The <code>$(command)</code> format doesn't change when nested and is |
| 624 | easier to read. |
| 625 | </p> |
| 626 | <p> |
| 627 | Example: |
| 628 | <CODE_SNIPPET> |
| 629 | # This is preferred: |
| 630 | var="$(command "$(command1)")" |
| 631 | |
| 632 | # This is not: |
| 633 | var="`command \`command1\``" |
| 634 | </CODE_SNIPPET> |
| 635 | </p> |
| 636 | </BODY> |
| 637 | </STYLEPOINT> |
| 638 | |
| 639 | <STYLEPOINT title="Test, [ and [["> |
| 640 | <SUMMARY> |
| 641 | <code>[[ ... ]]</code> is preferred over <code>[</code>, |
| 642 | <code>test</code> and <code>/usr/bin/[</code>. |
| 643 | </SUMMARY> |
| 644 | <BODY> |
| 645 | <p> |
| 646 | <code>[[ ... ]]</code> reduces errors as no pathname expansion or word |
| 647 | splitting takes place between <code>[[</code> and <code>]]</code> and |
| 648 | <code>[[ ... ]]</code> allows for regular expression matching where |
| 649 | <code>[ ... ]</code> does not. |
| 650 | <CODE_SNIPPET> |
| 651 | # This ensures the string on the left is made up of characters in the |
| 652 | # alnum character class followed by the string name. |
| 653 | # Note that the RHS should not be quoted here. |
| 654 | # For the gory details, see |
Ackermann Yuriy | 7969290 | 2016-04-01 21:41:34 +1300 | [diff] [blame] | 655 | # E14 at https://tiswww.case.edu/php/chet/bash/FAQ |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 656 | if [[ "filename" =~ ^[[:alnum:]]+name ]]; then |
| 657 | echo "Match" |
| 658 | fi |
| 659 | |
| 660 | # This matches the exact pattern "f*" (Does not match in this case) |
| 661 | if [[ "filename" == "f*" ]]; then |
| 662 | echo "Match" |
| 663 | fi |
| 664 | |
| 665 | # This gives a "too many arguments" error as f* is expanded to the |
| 666 | # contents of the current directory |
| 667 | if [ "filename" == f* ]; then |
| 668 | echo "Match" |
| 669 | fi |
| 670 | </CODE_SNIPPET> |
| 671 | </p> |
| 672 | </BODY> |
| 673 | </STYLEPOINT> |
| 674 | |
| 675 | <STYLEPOINT title="Testing Strings"> |
| 676 | <SUMMARY> |
| 677 | Use quotes rather than filler characters where possible. |
| 678 | </SUMMARY> |
| 679 | <BODY> |
| 680 | <p> |
| 681 | Bash is smart enough to deal with an empty string in a test. So, given |
| 682 | that the code is much easier to read, use tests for empty/non-empty |
| 683 | strings or empty strings rather than filler characters. |
| 684 | <CODE_SNIPPET> |
| 685 | # Do this: |
| 686 | if [[ "${my_var}" = "some_string" ]]; then |
| 687 | do_something |
| 688 | fi |
| 689 | |
| 690 | # -z (string length is zero) and -n (string length is not zero) are |
| 691 | # preferred over testing for an empty string |
| 692 | if [[ -z "${my_var}" ]]; then |
| 693 | do_something |
| 694 | fi |
| 695 | |
| 696 | # This is OK (ensure quotes on the empty side), but not preferred: |
| 697 | if [[ "${my_var}" = "" ]]; then |
| 698 | do_something |
| 699 | fi |
| 700 | |
| 701 | # Not this: |
| 702 | if [[ "${my_var}X" = "some_stringX" ]]; then |
| 703 | do_something |
| 704 | fi |
| 705 | </CODE_SNIPPET> |
| 706 | </p> |
| 707 | <p> |
| 708 | To avoid confusion about what you're testing for, explicitly use |
| 709 | <code>-z</code> or <code>-n</code>. |
| 710 | <CODE_SNIPPET> |
| 711 | # Use this |
| 712 | if [[ -n "${my_var}" ]]; then |
| 713 | do_something |
| 714 | fi |
| 715 | |
| 716 | # Instead of this as errors can occur if ${my_var} expands to a test |
| 717 | # flag |
| 718 | if [[ "${my_var}" ]]; then |
| 719 | do_something |
| 720 | fi |
| 721 | </CODE_SNIPPET> |
| 722 | </p> |
| 723 | </BODY> |
| 724 | </STYLEPOINT> |
| 725 | |
| 726 | <STYLEPOINT title="Wildcard Expansion of Filenames"> |
| 727 | <SUMMARY> |
| 728 | Use an explicit path when doing wildcard expansion of filenames. |
| 729 | </SUMMARY> |
| 730 | <BODY> |
| 731 | <p> |
| 732 | As filenames can begin with a <code>-</code>, it's a lot safer to |
| 733 | expand wildcards with <code>./*</code> instead of <code>*</code>. |
| 734 | <CODE_SNIPPET> |
| 735 | # Here's the contents of the directory: |
| 736 | # -f -r somedir somefile |
| 737 | |
| 738 | # This deletes almost everything in the directory by force |
| 739 | psa@bilby$ rm -v * |
| 740 | removed directory: `somedir' |
| 741 | removed `somefile' |
| 742 | |
| 743 | # As opposed to: |
| 744 | psa@bilby$ rm -v ./* |
| 745 | removed `./-f' |
| 746 | removed `./-r' |
| 747 | rm: cannot remove `./somedir': Is a directory |
| 748 | removed `./somefile' |
| 749 | </CODE_SNIPPET> |
| 750 | </p> |
| 751 | </BODY> |
| 752 | </STYLEPOINT> |
| 753 | |
| 754 | <STYLEPOINT title="Eval"> |
| 755 | <SUMMARY> |
| 756 | <code>eval</code> should be avoided. |
| 757 | </SUMMARY> |
| 758 | <BODY> |
| 759 | <p> |
| 760 | Eval munges the input when used for assignment to variables and can set |
| 761 | variables without making it possible to check what those variables |
| 762 | were. |
| 763 | <CODE_SNIPPET> |
| 764 | # What does this set? |
| 765 | # Did it succeed? In part or whole? |
| 766 | eval $(set_my_variables) |
| 767 | |
| 768 | # What happens if one of the returned values has a space in it? |
| 769 | variable="$(eval some_function)" |
| 770 | </CODE_SNIPPET> |
| 771 | </p> |
| 772 | </BODY> |
| 773 | </STYLEPOINT> |
| 774 | |
| 775 | <STYLEPOINT title="Pipes to While"> |
| 776 | <SUMMARY> |
| 777 | Use process substitution or for loops in preference to piping to while. |
| 778 | Variables modified in a while loop do not propagate to the parent |
| 779 | because the loop's commands run in a subshell. |
| 780 | </SUMMARY> |
| 781 | <BODY> |
| 782 | <p> |
| 783 | The implicit subshell in a pipe to while can make it difficult to track |
| 784 | down bugs. |
| 785 | <BAD_CODE_SNIPPET> |
| 786 | last_line='NULL' |
| 787 | your_command | while read line; do |
| 788 | last_line="${line}" |
| 789 | done |
| 790 | |
| 791 | # This will output 'NULL' |
| 792 | echo "${last_line}" |
| 793 | </BAD_CODE_SNIPPET> |
| 794 | </p> |
| 795 | <p> |
| 796 | Use a for loop if you are confident that the input will not contain |
| 797 | spaces or special characters (usually, this means not user input). |
| 798 | <CODE_SNIPPET> |
| 799 | total=0 |
| 800 | # Only do this if there are no spaces in return values. |
| 801 | for value in $(command); do |
| 802 | total+="${value}" |
| 803 | done |
| 804 | </CODE_SNIPPET> |
| 805 | </p> |
| 806 | <p> |
| 807 | Using process substitution allows redirecting output but puts the |
| 808 | commands in an explicit subshell rather than the implicit subshell that |
| 809 | bash creates for the while loop. |
| 810 | <CODE_SNIPPET> |
| 811 | total=0 |
| 812 | last_file= |
| 813 | while read count filename; do |
| 814 | total+="${count}" |
| 815 | last_file="${filename}" |
| 816 | done < <(your_command | uniq -c) |
| 817 | |
| 818 | # This will output the second field of the last line of output from |
| 819 | # the command. |
| 820 | echo "Total = ${total}" |
| 821 | echo "Last one = ${last_file}" |
| 822 | </CODE_SNIPPET> |
| 823 | </p> |
| 824 | <p> |
| 825 | Use while loops where it is not necessary to pass complex results |
| 826 | to the parent shell - this is typically where some more complex |
| 827 | "parsing" is required. Beware that simple examples are probably |
| 828 | more easily done with a tool such as awk. This may also be useful |
| 829 | where you specifically don't want to change the parent scope variables. |
| 830 | <CODE_SNIPPET> |
| 831 | # Trivial implementation of awk expression: |
| 832 | # awk '$3 == "nfs" { print $2 " maps to " $1 }' /proc/mounts |
| 833 | cat /proc/mounts | while read src dest type opts rest; do |
| 834 | if [[ ${type} == "nfs" ]]; then |
| 835 | echo "NFS ${dest} maps to ${src}" |
| 836 | fi |
| 837 | done |
| 838 | </CODE_SNIPPET> |
| 839 | </p> |
| 840 | </BODY> |
| 841 | </STYLEPOINT> |
| 842 | |
| 843 | </CATEGORY> |
| 844 | |
| 845 | <CATEGORY title="Naming Conventions"> |
| 846 | |
| 847 | <STYLEPOINT title="Function Names"> |
| 848 | <SUMMARY> |
| 849 | Lower-case, with underscores to separate words. Separate libraries |
| 850 | with <code>::</code>. Parentheses are required after the function name. |
| 851 | The keyword <code>function</code> is optional, but must be used |
| 852 | consistently throughout a project. |
| 853 | </SUMMARY> |
| 854 | <BODY> |
| 855 | <p> |
| 856 | If you're writing single functions, use lowercase and separate words |
| 857 | with underscore. If you're writing a package, separate package names |
| 858 | with <code>::</code>. Braces must be on the same line as the function |
| 859 | name (as with other languages at Google) and no space between the |
| 860 | function name and the parenthesis. |
| 861 | <CODE_SNIPPET> |
| 862 | # Single function |
| 863 | my_func() { |
| 864 | ... |
| 865 | } |
| 866 | |
| 867 | # Part of a package |
| 868 | mypackage::my_func() { |
| 869 | ... |
| 870 | } |
| 871 | </CODE_SNIPPET> |
| 872 | </p> |
| 873 | <p> |
| 874 | The <code>function</code> keyword is extraneous when "()" is present |
| 875 | after the function name, but enhances quick identification of |
| 876 | functions. |
| 877 | </p> |
| 878 | </BODY> |
| 879 | </STYLEPOINT> |
| 880 | |
| 881 | <STYLEPOINT title="Variable Names"> |
| 882 | <SUMMARY> |
| 883 | As for function names. |
| 884 | </SUMMARY> |
| 885 | <BODY> |
| 886 | <p> |
| 887 | Variables names for loops should be similarly named for any variable |
| 888 | you're looping through. |
| 889 | <CODE_SNIPPET> |
| 890 | for zone in ${zones}; do |
| 891 | something_with "${zone}" |
| 892 | done |
| 893 | </CODE_SNIPPET> |
| 894 | </p> |
| 895 | </BODY> |
| 896 | </STYLEPOINT> |
| 897 | |
| 898 | <STYLEPOINT title="Constants and Environment Variable Names"> |
| 899 | <SUMMARY> |
| 900 | All caps, separated with underscores, declared at the top of the file. |
| 901 | </SUMMARY> |
| 902 | <BODY> |
| 903 | <p> |
| 904 | Constants and anything exported to the environment should be |
| 905 | capitalized. |
| 906 | <CODE_SNIPPET> |
| 907 | # Constant |
| 908 | readonly PATH_TO_FILES='/some/path' |
| 909 | |
| 910 | # Both constant and environment |
| 911 | declare -xr ORACLE_SID='PROD' |
| 912 | </CODE_SNIPPET> |
| 913 | </p> |
| 914 | <p> |
| 915 | Some things become constant at their first setting (for example, via |
| 916 | getopts). Thus, it's OK to set a constant in getopts or based on a |
| 917 | condition, but it should be made readonly immediately afterwards. |
| 918 | Note that <code>declare</code> doesn't operate on global variables |
| 919 | within functions, so <code>readonly</code> or <code>export</code> is |
| 920 | recommended instead. |
| 921 | </p> |
| 922 | <CODE_SNIPPET> |
| 923 | VERBOSE='false' |
| 924 | while getopts 'v' flag; do |
| 925 | case "${flag}" in |
mark@chromium.org | 7b24563 | 2013-09-25 21:16:00 +0000 | [diff] [blame] | 926 | v) VERBOSE='true' ;; |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 927 | esac |
| 928 | done |
| 929 | readonly VERBOSE |
| 930 | </CODE_SNIPPET> |
| 931 | </BODY> |
| 932 | </STYLEPOINT> |
| 933 | |
| 934 | <STYLEPOINT title="Source Filenames"> |
| 935 | <SUMMARY> |
| 936 | Lowercase, with underscores to separate words if desired. |
| 937 | </SUMMARY> |
| 938 | <BODY> |
| 939 | <p> |
| 940 | This is for consistency with other code styles in Google: |
| 941 | <code>maketemplate</code> or <code>make_template</code> but not |
| 942 | <code>make-template</code>. |
| 943 | </p> |
| 944 | </BODY> |
| 945 | </STYLEPOINT> |
| 946 | |
| 947 | <STYLEPOINT title="Read-only Variables"> |
| 948 | <SUMMARY> |
| 949 | Use <code>readonly</code> or <code>declare -r</code> to ensure they're |
| 950 | read only. |
| 951 | </SUMMARY> |
| 952 | <BODY> |
| 953 | <p> |
| 954 | As globals are widely used in shell, it's important to catch errors |
| 955 | when working with them. When you declare a variable that is |
| 956 | meant to be read-only, make this explicit. |
| 957 | <CODE_SNIPPET> |
| 958 | zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)" |
| 959 | if [[ -z "${zip_version}" ]]; then |
| 960 | error_message |
| 961 | else |
| 962 | readonly zip_version |
| 963 | fi |
| 964 | </CODE_SNIPPET> |
| 965 | </p> |
| 966 | </BODY> |
| 967 | </STYLEPOINT> |
| 968 | |
| 969 | <STYLEPOINT title="Use Local Variables"> |
| 970 | <SUMMARY> |
| 971 | Declare function-specific variables with <code>local</code>. Declaration |
| 972 | and assignment should be on different lines. |
| 973 | </SUMMARY> |
| 974 | <BODY> |
| 975 | <p> |
| 976 | Ensure that local variables are only seen inside a function and its |
| 977 | children by using <code>local</code> when declaring them. This avoids |
| 978 | polluting the global name space and inadvertently setting variables |
| 979 | that may have significance outside the function. |
| 980 | </p> |
| 981 | <p> |
| 982 | Declaration and assignment must be separate statements when |
| 983 | the assignment value is provided by a command substitution; as |
| 984 | the 'local' builtin does not propagate the exit code from the |
| 985 | command substitution. |
| 986 | <CODE_SNIPPET> |
| 987 | my_func2() { |
| 988 | local name="$1" |
| 989 | |
| 990 | # Separate lines for declaration and assignment: |
| 991 | local my_var |
| 992 | my_var="$(my_func)" || return |
| 993 | |
| 994 | # DO NOT do this: $? contains the exit code of 'local', not my_func |
| 995 | local my_var="$(my_func)" |
| 996 | [[ $? -eq 0 ]] || return |
| 997 | |
| 998 | ... |
| 999 | } |
| 1000 | </CODE_SNIPPET> |
| 1001 | </p> |
| 1002 | </BODY> |
| 1003 | </STYLEPOINT> |
| 1004 | |
| 1005 | <STYLEPOINT title="Function Location"> |
| 1006 | <SUMMARY> |
| 1007 | Put all functions together in the file just below constants. Don't hide |
| 1008 | executable code between functions. |
| 1009 | </SUMMARY> |
| 1010 | <BODY> |
| 1011 | <p> |
| 1012 | If you've got functions, put them all together near the top of the |
| 1013 | file. Only includes, <code>set</code> statements and setting constants |
| 1014 | may be done before declaring functions. |
| 1015 | </p> |
| 1016 | <p> |
| 1017 | Don't hide executable code between functions. Doing so makes the code |
| 1018 | difficult to follow and results in nasty surprises when debugging. |
| 1019 | </p> |
| 1020 | </BODY> |
| 1021 | </STYLEPOINT> |
| 1022 | |
| 1023 | <STYLEPOINT title="main"> |
| 1024 | <SUMMARY> |
| 1025 | A function called <code>main</code> is required for scripts long enough |
| 1026 | to contain at least one other function. |
| 1027 | </SUMMARY> |
| 1028 | <BODY> |
| 1029 | <p> |
| 1030 | In order to easily find the start of the program, put the main |
| 1031 | program in a function called <code>main</code> as the bottom most |
| 1032 | function. This provides consistency with the rest of the code base as |
| 1033 | well as allowing you to define more variables as <code>local</code> |
| 1034 | (which can't be done if the main code is not a function). The last |
| 1035 | non-comment line in the file should be a call to <code>main</code>: |
| 1036 | <CODE_SNIPPET> |
| 1037 | main "$@" |
| 1038 | </CODE_SNIPPET> |
| 1039 | </p> |
| 1040 | <p> |
| 1041 | Obviously, for short scripts where it's just a linear flow, |
| 1042 | <code>main</code> is overkill and so is not required. |
| 1043 | </p> |
| 1044 | </BODY> |
| 1045 | </STYLEPOINT> |
| 1046 | |
| 1047 | </CATEGORY> |
| 1048 | |
| 1049 | <CATEGORY title="Calling Commands"> |
| 1050 | |
| 1051 | <STYLEPOINT title="Checking Return Values"> |
| 1052 | <SUMMARY> |
| 1053 | Always check return values and give informative return values. |
| 1054 | </SUMMARY> |
| 1055 | <BODY> |
| 1056 | <p> |
| 1057 | For unpiped commands, use <code>$?</code> or check directly via an |
| 1058 | <code>if</code> statement to keep it simple. |
| 1059 | </p> |
| 1060 | <p> |
| 1061 | Example: |
| 1062 | <CODE_SNIPPET> |
| 1063 | if ! mv "${file_list}" "${dest_dir}/" ; then |
| 1064 | echo "Unable to move ${file_list} to ${dest_dir}" >&2 |
| 1065 | exit "${E_BAD_MOVE}" |
| 1066 | fi |
| 1067 | |
| 1068 | # Or |
| 1069 | mv "${file_list}" "${dest_dir}/" |
| 1070 | if [[ "$?" -ne 0 ]]; then |
| 1071 | echo "Unable to move ${file_list} to ${dest_dir}" >&2 |
| 1072 | exit "${E_BAD_MOVE}" |
| 1073 | fi |
| 1074 | |
| 1075 | </CODE_SNIPPET> |
| 1076 | </p> |
| 1077 | <p> |
| 1078 | Bash also has the <code>PIPESTATUS</code> variable that allows checking |
| 1079 | of the return code from all parts of a pipe. If it's only necessary to |
| 1080 | check success or failure of the whole pipe, then the following is |
| 1081 | acceptable: |
| 1082 | <CODE_SNIPPET> |
| 1083 | tar -cf - ./* | ( cd "${dir}" && tar -xf - ) |
| 1084 | if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then |
| 1085 | echo "Unable to tar files to ${dir}" >&2 |
| 1086 | fi |
| 1087 | </CODE_SNIPPET> |
| 1088 | </p> |
| 1089 | <p> |
| 1090 | However, as <code>PIPESTATUS</code> will be overwritten as soon as you |
| 1091 | do any other command, if you need to act differently on errors based on |
| 1092 | where it happened in the pipe, you'll need to assign |
| 1093 | <code>PIPESTATUS</code> to another variable immediately after running |
| 1094 | the command (don't forget that <code>[</code> is a command and will |
| 1095 | wipe out <code>PIPESTATUS</code>). |
| 1096 | <CODE_SNIPPET> |
| 1097 | tar -cf - ./* | ( cd "${DIR}" && tar -xf - ) |
| 1098 | return_codes=(${PIPESTATUS[*]}) |
| 1099 | if [[ "${return_codes[0]}" -ne 0 ]]; then |
| 1100 | do_something |
| 1101 | fi |
| 1102 | if [[ "${return_codes[1]}" -ne 0 ]]; then |
| 1103 | do_something_else |
| 1104 | fi |
| 1105 | </CODE_SNIPPET> |
| 1106 | </p> |
| 1107 | </BODY> |
| 1108 | </STYLEPOINT> |
| 1109 | |
| 1110 | <STYLEPOINT title="Builtin Commands vs. External Commands"> |
| 1111 | <SUMMARY> |
| 1112 | Given the choice between invoking a shell builtin and invoking a separate |
| 1113 | process, choose the builtin. |
| 1114 | </SUMMARY> |
| 1115 | <BODY> |
| 1116 | <p> |
| 1117 | We prefer the use of builtins such as the <em>Parameter Expansion</em> |
| 1118 | functions in <code>bash(1)</code> as it's more robust and portable |
| 1119 | (especially when compared to things like sed). |
| 1120 | </p> |
| 1121 | <p> |
| 1122 | Example: |
| 1123 | <CODE_SNIPPET> |
| 1124 | # Prefer this: |
| 1125 | addition=$((${X} + ${Y})) |
| 1126 | substitution="${string/#foo/bar}" |
| 1127 | |
| 1128 | # Instead of this: |
| 1129 | addition="$(expr ${X} + ${Y})" |
| 1130 | substitution="$(echo "${string}" | sed -e 's/^foo/bar/')" |
| 1131 | </CODE_SNIPPET> |
| 1132 | </p> |
| 1133 | </BODY> |
| 1134 | </STYLEPOINT> |
| 1135 | |
| 1136 | </CATEGORY> |
| 1137 | |
| 1138 | <CATEGORY title="Conclusion"> |
| 1139 | <p> |
| 1140 | Use common sense and <em>BE CONSISTENT</em>. |
| 1141 | </p> |
| 1142 | <p> |
| 1143 | Please take a few minutes to read the Parting Words section at the bottom |
Alexander F Rødseth | 7ebdcc4 | 2016-05-19 23:04:17 +0200 | [diff] [blame] | 1144 | of the <a href="cppguide.html">C++ Guide</a>. |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 1145 | </p> |
| 1146 | </CATEGORY> |
| 1147 | |
| 1148 | <p align="right"> |
mark@chromium.org | 7b24563 | 2013-09-25 21:16:00 +0000 | [diff] [blame] | 1149 | Revision 1.26 |
mark@chromium.org | 7cf216c | 2013-02-15 20:56:05 +0000 | [diff] [blame] | 1150 | </p> |
| 1151 | |
| 1152 | </GUIDE> |