Added "check_cd" function to "fx doctor"

verify "cd" does not output anything to stdout, since many
other scripts assume this.

Note that check_cd optionally prints recommendations for
resolving the issue within the developer's shell environment.

For bash users only, doctor calls
scripts/devshell/lib/bashrc_checkup.sh,
which will attempt to load their ~/.bashrc
file, to allow doctor to check for conflicts in the interactive
shell environment (e.g., functions like overriding "cd").

For users using non-bash shells (like zsh) this current iteration
of "doctor" will still check environment variables and files
through the non-interactive bash script, but we may want to add
a "zshrc_checkup.zsh" script as well if we want to check for zsh-specific
interactive shell conflicts. (The bash "doctor" could still
launch other non-bash shell checks.)

Updated "doctor" script format and error handling a bit. Needs more
work to be easier to use and easier to add more checks.

Added --indent flag to style.sh and moved common terminal output
style functions, originally in doctor, to
devshell/lib/common_term_styles.sh, for reusability.

Change-Id: I3b054609e87274f49479c93c3a6c2e29bb7136eb
diff --git a/devshell/doctor b/devshell/doctor
index ed7ef38..d264754 100755
--- a/devshell/doctor
+++ b/devshell/doctor
@@ -8,42 +8,93 @@
 ## usage: fx doctor
 
 # The goal of this script is to detect common issues with a Fuchsia
-# checkout. For example, on OS X the xcode command line tool
+# checkout and potential conflicts in the user's shell environment.
+#
+# For example, on OS X the xcode command line tool
 # installation often lapses. Ensuring that `xcode select --install` is
 # run as part of a checkout or build is problematic: the step involves
 # manual input. Detecting that it needs to be run, however, is
 # perfectly mechanizable.
+#
+# For potential issues in the user's shell initialization script
+# (such as ~/.bashrc), this script will also run a shell checkup
+# script (for example, devshell/lib/bashrc_checkup.sh)
+# under the user's bash "${SHELL}" (if different from /bin/bash),
+# load the user's shell settings, and check for any known issues.
 
-source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"/lib/vars.sh
-fx-config-read
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/lib/vars.sh || exit $?
+source "${FUCHSIA_DIR}/scripts/devshell/lib/style.sh" || exit $?
+source "${FUCHSIA_DIR}/scripts/devshell/lib/common_term_styles.sh" || exit $?
 
-function dr-mac {
-    # TODO actually check the need for this
-    echo "A common issue with Fuchsia development on macOS is needing to"
-    echo "re-run the \`xcode-select install\` step. The typical symptom is"
-    echo "failure to find system C or C++ headers after a reboot or update."
-    return
+fx-config-read || exit $?
+
+dr_mac() {
+  local status=0
+  # TODO actually check the need for this
+
+  info "Make sure you've run \`xcode-select install\`"
+
+  details << EOF
+A common issue with Fuchsia development on macOS is needing to
+re-run the \`xcode-select install\` step. The typical symptom is
+failure to find system C or C++ headers after a reboot or update.
+
+See $(link 'https://fuchsia.googlesource.com/docs/getting_started.md#macos')
+for more details.
+EOF
+
+  return ${status}
 }
 
-function dr-linux {
-    return
+dr_linux() {
+  local status=0
+  return ${status}
 }
 
-function dr {
-    return
+shell_checkup() {
+  local status=0
+
+  # If the user is using bash, their default interactive "${SHELL}"
+  # may differ from the script-standard "/bin/bash", and their ~/.bashrc
+  # may depend on features of their shell that are not present in
+  # /bin/bash, so launch the shell checkup script using "${SHELL}".
+  #
+  # For example, since MacOS includes only bash version 3, Homebrew users
+  # may install bash 4 in /usr/local/bin/bash, and then select
+  # bash 4 by adding it to /etc/shells, and running the "chsh" command.
+
+  local shell_type="$(basename "${SHELL}")"
+  case "${shell_type}" in
+    bash)
+      local current_debug_flag="$(echo $-|sed -n 's/.*x.*/-x/p')"
+      eval "${SHELL}" "${current_debug_flag}" "${FUCHSIA_DIR}/scripts/devshell/lib/bashrc_checkup.sh" || status=$?
+      ;;
+    *)
+      info "No shell checkup for ${shell_type}"
+      ;;
+  esac
+
+  return ${status}
 }
 
-function main {
-    case $(uname) in
-        Darwin)
-            dr-mac
-            ;;
-        Linux)
-            dr-linux
-            ;;
-    esac
-    dr
-    return
+dr_all() {
+  local status=0
+  shell_checkup || status=$?
+  return ${status}
 }
 
-main "$@"
+main() {
+  local status=0
+  case $(uname) in
+    Darwin)
+      dr_mac || status=$?
+      ;;
+    Linux)
+      dr_linux || status=$?
+      ;;
+  esac
+  dr_all || status=$?
+  return ${status}
+}
+
+main "$@" || exit $?
diff --git a/devshell/lib/bashrc_checkup.sh b/devshell/lib/bashrc_checkup.sh
new file mode 100644
index 0000000..33915d0
--- /dev/null
+++ b/devshell/lib/bashrc_checkup.sh
@@ -0,0 +1,129 @@
+# No #!/bin/bash - See "usage"
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+### check the health of a user's interactive bash environment (from "fx doctor")
+
+## usage:
+##   "${SHELL}" [-flags] "${FUCHSIA_DIR}/scripts/devshell/lib/bashrc_checkup.sh" || status=$?
+##   (Valid only for bash ${SHELL} since this script is bash.)
+
+# Detect potential problems for Fuchsia development from settings specific
+# to the user's interactive shell environment. Potential customizations can
+# include bash version and settings introduced in the user's ~/.bashrc file
+# such as bash functions, aliases, and non-exported variables such as
+# "${CDPATH}" and "${PATH}" that can impact how bash executes some commands
+# from the command line.
+#
+
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/vars.sh || exit $?
+source "${FUCHSIA_DIR}/scripts/devshell/lib/style.sh" || exit $?
+source "${FUCHSIA_DIR}/scripts/devshell/lib/common_term_styles.sh" || exit $?
+
+fx-config-read || exit $?
+
+# For bash users, this script also attempts to load your preferred
+# bash interpreter (if different from the default, such as Homebrew
+# bash on Mac), and load your ~/.bashrc settings, as would happen
+# in an interactive shell. This allows doctor to check for
+# potential issues with settings that don't normally propagate to
+# bash scripts (unless executed with "source"), such as bash
+# functions, aliases, and unexported variables.
+
+# For bash users, load settings that would exist in the user's
+# interactive bash shells as per
+# [The GNU Bash Reference Manual, for Bash, Version 4.4](https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html)
+if [ -f ~/.bashrc ]; then
+  source ~/.bashrc
+fi
+
+check_cd() {
+  # Returns an error status if the current definition of "cd"
+  # writes anything to the stdout stream, which would break common bash
+  # script lines similar to the following:
+  #
+  #   SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+  local cd_output=$(
+    CDPATH=""
+    cd "${FUCHSIA_DIR}" 2>/dev/null
+    cd scripts 2>/dev/null
+  )
+
+  local cdpath_output=$(
+    if [ -z "${cd_output}" ] && [ "${CDPATH}" != "" ]; then
+      CDPATH="${FUCHSIA_DIR}"
+      cd scripts 2>/dev/null
+    fi
+  )
+
+  if [ -z "${cdpath_output}" ] && [ -z "${cd_output}" ]; then
+    return 0  # The check passed!
+  fi
+
+  # The check failed. Print recommendations based on what we found.
+
+  local status=1
+
+  warn 'Your implementation of the "cd" command writes to stdout.'
+
+  details << EOF
+Many common developer scripts and tools use "cd" to find relative
+file paths and will fail in unpredictable ways.
+EOF
+
+  if [ ! -z "${cdpath_output}" ]; then
+    details << EOF
+
+The "cd" command writes to stdout based on your CDPATH environment variable.
+
+You can remove or unset CDPATH in your shell initialization script, or
+define a cd wrapper function.
+EOF
+  fi
+
+  details << EOF
+
+If you have not redefined "cd", and the builtin "cd" is writing to stdout,
+define a wrapper function and redirect the output to /dev/null or stderr.
+
+EOF
+  code << EOF
+cd() {
+  builtin cd "\$@"
+}
+EOF
+  details << EOF
+
+If you already redefine "cd" during shell initialization, find the alias,
+function, or script, and either remove it, or redirect the output to stderr
+by appending "", as in this example:
+
+EOF
+  code << EOF
+cd() {
+  builtin cd "\$@" >/dev/null
+  update_terminal_cwd
+}
+EOF
+  details << EOF
+
+(Note, in this example, "update_terminal_cwd" is a common MacOS function
+to call when changing directories. Other common "cd" overrides may invoke
+"pwd", "print", or other commands.)
+
+EOF
+
+  return ${status}
+}
+
+main() {
+  local status=0
+
+  check_cd || status=$?
+
+  return ${status}
+}
+
+main "$@" || exit $?
diff --git a/devshell/lib/common_term_styles.sh b/devshell/lib/common_term_styles.sh
new file mode 100644
index 0000000..26940f4
--- /dev/null
+++ b/devshell/lib/common_term_styles.sh
@@ -0,0 +1,64 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+### add-on functions for common styling of text messages to the terminal
+
+## "source style.sh" before sourcing this script.
+## Functions include:
+##
+## * info
+## * warn
+## * error
+## * link
+## * code
+## * details
+
+## usage examples:
+##
+## # First import style.sh
+##
+## source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/lib/vars.sh || exit $?
+## source "${FUCHSIA_DIR}/scripts/devshell/lib/style.sh" || exit $?
+## source "${FUCHSIA_DIR}/scripts/devshell/lib/common_term_styles.sh" || exit $?
+##
+##  warn 'The warning message.'
+##
+##  details << EOF
+##A multi-line message with bash ${variable} expansion.
+##Excape dollars with backslash \$
+##See $(link 'https://some/hyper/link') to insert a link.
+##EOF
+##
+## Visual tests (and demonstration of capabilities) can be run from:
+##   //scripts/tests/common_term_styles-test-visually
+
+info() {
+  style::info --stdout "
+INFO: $@
+"
+}
+
+warn() {
+  style::warning --stdout "
+WARNING: $@
+"
+}
+
+error() {
+  style::error --stdout "
+ERROR: $@
+"
+}
+
+details() {
+  style::cat --indent 2
+}
+
+code() {
+  style::cat --bold --magenta --indent 4
+}
+
+link() {
+  style::link "$@"
+}
diff --git a/devshell/lib/style.sh b/devshell/lib/style.sh
index 7d9264c..8e0a2cf 100644
--- a/devshell/lib/style.sh
+++ b/devshell/lib/style.sh
@@ -11,6 +11,11 @@
 # with underline. The text is written to stderr instead of the default
 # stdout.
 #
+#   style::cat --color black --background cyan --indent 4 <<EOF
+#   Multi-line text with expanded bash ${variables}
+#   can be styled and indented.
+#   EOF
+#
 # style::info, style::warning, and style::error use echo to stderr
 # with default color and bold text. For example:
 #
@@ -98,6 +103,7 @@
   --bold, --faint, --underline, etc.
   --color <color_name>
   --background <color_name>
+  --indent <spaces_count>
   --stderr (output to standard error instead of standard out)
 
   echo "This is \$(style::echo -f --bold LOUD) and soft."
@@ -168,7 +174,9 @@
   local styles
   local semicolon
   local name
-  local -i code
+  local -i indent=0
+  local prefix
+  local -i code=0
 
   while $get_flags; do
     case "$1" in
@@ -190,6 +198,10 @@
         styles="${styles}${semicolon}$(style::background $name || exit $?)" || return $?
         semicolon=';'
         ;;
+      --indent)
+        shift; indent=$1; shift
+        prefix="$(printf "%${indent}s")"
+        ;;
       --*)
         name="${1:2}"
         code=$(style::attribute $name 0)
@@ -216,7 +228,14 @@
 
   if [ ! -t ${fd} ] && ${STYLE_TO_TTY_ONLY}; then
     # Output is not to a TTY so don't stylize
-    >&${fd} "${command}" "$@" || return $?
+    if [[ "${prefix}" == "" ]]; then
+      >&${fd} "${command}" "$@" || status=$?
+    else
+      >&${fd} "${command}" "$@" | sed "s/^/${prefix}/"
+      if (( ${PIPESTATUS[0]} != 0 )); then
+        status=${PIPESTATUS[0]}
+      fi
+    fi
     return 0
   fi
 
@@ -225,6 +244,9 @@
 
   # Add placeholder (.) so command substitution doesn't strip trailing newlines
   text="$("${command}" "$@" || exit $?;echo -n '.')" || return $?
+  if [[ "${prefix}" != "" ]]; then
+    text="$(echo "${text}" | sed "s/^/${prefix}/;\$s/^${prefix}[.]\$/./")"
+  fi
 
   local -i len=$((${#text}-2))
   if [[ "${text:$len:1}" == $'\n' ]]; then
diff --git a/tests/common_term_styles-test-visually b/tests/common_term_styles-test-visually
new file mode 100755
index 0000000..32d8f37
--- /dev/null
+++ b/tests/common_term_styles-test-visually
@@ -0,0 +1,32 @@
+#!/bin/bash
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Visual tests for //scripts/devshell/lib/common_term_styles.sh
+
+# This is not an automated unit test.
+# It prints stylized to demonstrate the styles on a terminal.
+
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")"/../devshell/lib >/dev/null 2>&1 && pwd)"/style.sh || exit $?
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")"/../devshell/lib >/dev/null 2>&1 && pwd)"/common_term_styles.sh || exit $?
+
+runtest() {
+  command="$1"; shift
+  echo "${command}" "$@"
+  ${command} "$@"
+}
+
+runtest info 'This is informational'
+runtest warn 'This is your last warning'
+runtest error 'Danger! Danger Will Robinson!'
+runtest details <<EOF
+This detail will be
+indented from the error.
+and could have a link like $(link 'https://some/url/here')
+EOF
+runtest code <<EOF
+for ( line in lines_of_code ) {
+  print "this is the demon of code style"
+end
+EOF
diff --git a/tests/common_term_styles-tests b/tests/common_term_styles-tests
new file mode 100755
index 0000000..b51550d
--- /dev/null
+++ b/tests/common_term_styles-tests
@@ -0,0 +1,40 @@
+#!/bin/bash
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Automated tests for //scripts/devshell/lib/common_term_styles.sh
+#
+# Usage: common_term_styles-tests
+#
+#   Returns: Error status if actual output does not match expected.
+
+TEST_NAME="$(basename "${BASH_SOURCE[0]}")"
+TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+
+verbose() {
+  echo
+  echo "======================================================="
+  echo
+  echo "$@"
+  echo
+  "$@"
+  echo
+}
+
+test_main() {
+  local expected_out="${TESTS_DIR}/expected/${TEST_NAME}.out"
+  local expected_err="${TESTS_DIR}/expected/${TEST_NAME}.err"
+  local capture_dir=$(mktemp -d)
+  local actual_out="${capture_dir}/${TEST_NAME}.out"
+  local actual_err="${capture_dir}/${TEST_NAME}.err"
+  ${TESTS_DIR}/common_term_styles-test-visually 1> "${actual_out}" 2> "${actual_err}"
+
+  local status=0
+  verbose diff "${expected_out}" "${actual_out}" || status=$?
+  verbose diff "${expected_err}" "${actual_err}" || status=$?
+
+  return $status
+}
+
+test_main "$@" || return $?
diff --git a/tests/expected/common_term_styles-tests.err b/tests/expected/common_term_styles-tests.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/expected/common_term_styles-tests.err
diff --git a/tests/expected/common_term_styles-tests.out b/tests/expected/common_term_styles-tests.out
new file mode 100644
index 0000000..4cd26e1
--- /dev/null
+++ b/tests/expected/common_term_styles-tests.out
@@ -0,0 +1,20 @@
+info This is informational
+
+INFO: This is informational
+
+warn This is your last warning
+
+WARNING: This is your last warning
+
+error Danger! Danger Will Robinson!
+
+ERROR: Danger! Danger Will Robinson!
+
+details
+  This detail will be
+  indented from the error.
+  and could have a link like https://some/url/here
+code
+    for ( line in lines_of_code ) {
+      print "this is the demon of code style"
+    end
diff --git a/tests/expected/style-tests.err b/tests/expected/style-tests.err
index b921ea7..c7e4d76 100644
--- a/tests/expected/style-tests.err
+++ b/tests/expected/style-tests.err
@@ -8,6 +8,7 @@
   --bold, --faint, --underline, etc.
   --color <color_name>
   --background <color_name>
+  --indent <spaces_count>
   --stderr (output to standard error instead of standard out)
 
   echo "This is $(style::echo -f --bold LOUD) and soft."
@@ -23,6 +24,7 @@
   --bold, --faint, --underline, etc.
   --color <color_name>
   --background <color_name>
+  --indent <spaces_count>
   --stderr (output to standard error instead of standard out)
 
   echo "This is $(style::echo -f --bold LOUD) and soft."
@@ -41,6 +43,7 @@
   --bold, --faint, --underline, etc.
   --color <color_name>
   --background <color_name>
+  --indent <spaces_count>
   --stderr (output to standard error instead of standard out)
 
   echo "This is $(style::echo -f --bold LOUD) and soft."
@@ -59,6 +62,7 @@
   --bold, --faint, --underline, etc.
   --color <color_name>
   --background <color_name>
+  --indent <spaces_count>
   --stderr (output to standard error instead of standard out)
 
   echo "This is $(style::echo -f --bold LOUD) and soft."
diff --git a/tests/expected/style-tests.out b/tests/expected/style-tests.out
index 87424d7..2652af8 100644
--- a/tests/expected/style-tests.out
+++ b/tests/expected/style-tests.out
@@ -21,6 +21,9 @@
 Now is the time for all good
 people to come to the
 aid of their country and world.
+    Now is the time for all good
+    people to come to the
+    aid of their country and world.
 http://wikipedia.com
 STYLE_TO_TTY_ONLY=true
 
diff --git a/tests/style-test-visually b/tests/style-test-visually
index 876ea64..1209d0e 100755
--- a/tests/style-test-visually
+++ b/tests/style-test-visually
@@ -51,6 +51,12 @@
 aid of their country and world.
 EOF
 
+style::cat --background cyan --color black --indent 4 << EOF
+Now is the time for all good
+people to come to the
+aid of their country and world.
+EOF
+
 style::info 'INFO: Info here'
 style::warning 'WARNING: Watch out!'
 style::error 'ERROR: What went wrong now?'