Improved style.sh

Better reverse video for multiline text.

Usage and help including option to list style options.

Visual test script in //scripts/tests

Also added an automated test and "expected" files

Change-Id: Ie095c31e01bc3ebdc384d3d13d5d7fb151f948ce
diff --git a/devshell/lib/style.sh b/devshell/lib/style.sh
index 15a4d6d..7d9264c 100644
--- a/devshell/lib/style.sh
+++ b/devshell/lib/style.sh
@@ -22,10 +22,14 @@
 #
 #   export STYLE_WARNING="--stderr --faint --dark_red --background dark_yellow"
 #
+# Visual tests (and demonstration of capabilities) can be run from:
+#   //scripts/tests/style-test-visually
 
 # This script should be sourced. It is compatible with Bash 3.
 # MacOS still comes with Bash 3, so unfortunately no associative arrays.
 
+STYLE_TO_TTY_ONLY=false  # Set to true to suppress styling if output is redirected
+
 [[ "${STYLE_ERROR}" != "" ]] || STYLE_ERROR="--stderr --bold --color red"
 [[ "${STYLE_WARNING}" != "" ]] || STYLE_WARNING="--stderr --bold --color dark_yellow"
 [[ "${STYLE_INFO}" != "" ]] || STYLE_INFO="--stderr --bold --color dark_green"
@@ -58,6 +62,53 @@
 declare -i TERM_COLORS__cyan=96
 declare -i TERM_COLORS__white=97
 
+style::colors() {
+  set | sed -n "s/^TERM_COLORS__\([^=]*\)=.*$/\1/p" >&2
+}
+
+style::attributes() {
+  set | sed -n "s/^TERM_ATTRIBUTES__\([^=]*\)=.*$/--\1/p" >&2
+}
+
+style::usage() {
+  local help_option="$1"; shift
+  if [[ "${help_option}" == "colors" ]]; then
+    style::colors
+    return
+  elif [[ "${help_option}" == "attributes" ]]; then
+    style::attributes
+    return
+  fi
+  local function_call="$1"
+  local -a words=( $function_call )
+  local funcname="${words[0]}"
+  local command="$2"
+  local specifics="$3"
+
+  >&2 echo "
+Usage: ${function_call} [style options] [command parameters]"
+
+  if [[ "${specifics}" != "" ]]; then
+    >&2 echo "
+${specifics}"
+  fi
+  >&2 cat << EOF
+
+style options include:
+  --bold, --faint, --underline, etc.
+  --color <color_name>
+  --background <color_name>
+  --stderr (output to standard error instead of standard out)
+
+  echo "This is \$(style::echo -f --bold LOUD) and soft."
+
+command parameters are those supported by the ${command} command.
+
+Use ${funcname} --help colors for a list of colors or backgrounds
+Use ${funcname} --help attributes for a list of style attribute flags
+EOF
+}
+
 style::attribute() {
   local name="$1"
   local fallback="$2"
@@ -98,10 +149,19 @@
   echo $((10+${color}))
 }
 
-_STYLE_RESET="\033[0m"
-
 style::stylize() {
+  if [[ "$1" == --* || "$1" == "" ]]; then
+    style::usage "$2" "${FUNCNAME[0]} <command>" "stylized" "\
+<command> is any command with output to stylize, followed by style options,
+and then the command's normal parameters."
+    return
+  fi
+
   local command="$1"; shift
+  if [[ "$1" == "--help" ]]; then
+    style::usage "$2" "style::${command}" "'${command}'"
+    return
+  fi
 
   local get_flags=true
   local -i fd=1
@@ -116,6 +176,10 @@
         fd=2
         shift
         ;;
+      --stdout)
+        fd=1
+        shift
+        ;;
       --color)
         shift; name="$1"; shift
         styles="${styles}${semicolon}$(style::color $name || exit $?)" || return $?
@@ -150,33 +214,36 @@
     esac
   done
 
-  local status=0
-
-  if [ ! -t $fd ]; then
+  if [ ! -t ${fd} ] && ${STYLE_TO_TTY_ONLY}; then
     # Output is not to a TTY so don't stylize
-    >&${fd} "${command}" "$@" || status=$?
-    return $status
+    >&${fd} "${command}" "$@" || return $?
+    return 0
   fi
 
   local if_newline=''
   local text
+
   # Add placeholder (.) so command substitution doesn't strip trailing newlines
-  text="$("${command}" "$@" || exit $?;echo '.')" || status=$?
+  text="$("${command}" "$@" || exit $?;echo -n '.')" || return $?
+
   local -i len=$((${#text}-2))
   if [[ "${text:$len:1}" == $'\n' ]]; then
+    # Save last newline to add back after styling.
     if_newline='\n'
   else
     ((len++))
   fi
-  # Strip trailing newline, if any, and placeholder
-  # Last newline should not be stylized.
-  # TODO(richkadel): We may want to remove style from all newlines.
-  # Background color looks odd when newlines are styled.
+  # Strip trailing newline, if any, and placeholder.
   text="${text:0:$((len))}"
 
-  >&${fd} printf "\033[${styles}m%s${_STYLE_RESET}${if_newline}" "${text}"
+  # Style everything except newlines, otherwise background color highlights
+  # entire line. Add extra line with a character so sed does not add it's own
+  # last newline, then delete the line after substitutions.
+  local styled=$(printf '%s\n.' "${text}" | sed -e $'s/$/\033[0m/;s/^/\033['"${styles}"'m/;$d')
 
-  return $status
+  >&${fd} printf "%s${if_newline}" "${styled}"
+
+  return 0
 }
 
 style::echo() {
@@ -191,18 +258,32 @@
   style::stylize "${FUNCNAME[0]:7}" "$@" || return $?
 }
 
+style::_echo_with_styles() {
+  local funcname="$1";shift
+  local style_options="$1";shift
+  if [[ "$1" == "--help" ]]; then
+
+    style::usage "$2" "${funcname}" "echo" "\
+Default style options for ${funcname}:
+  $(style::echo ${style_options} --stdout \"${style_options}\")"
+
+    return
+  fi
+  style::echo ${style_options} "$@" || return $?
+}
+
 style::error() {
-  style::echo ${STYLE_ERROR} "$@" || return $?
+  style::_echo_with_styles "${FUNCNAME[0]}" "${STYLE_ERROR}" "$@" || return $?
 }
 
 style::warning() {
-  style::echo ${STYLE_WARNING} "$@" || return $?
+  style::_echo_with_styles "${FUNCNAME[0]}" "${STYLE_WARNING}" "$@" || return $?
 }
 
 style::info() {
-  style::echo ${STYLE_INFO} "$@" || return $?
+  style::_echo_with_styles "${FUNCNAME[0]}" "${STYLE_INFO}" "$@" || return $?
 }
 
 style::link() {
-  style::echo ${STYLE_LINK} "$@" || return $?
+  style::_echo_with_styles "${FUNCNAME[0]}" "${STYLE_LINK}" "$@" || return $?
 }
diff --git a/tests/expected/style-tests.err b/tests/expected/style-tests.err
new file mode 100644
index 0000000..b921ea7
--- /dev/null
+++ b/tests/expected/style-tests.err
@@ -0,0 +1,100 @@
+
+Usage: style::stylize <command> [style options] [command parameters]
+
+<command> is any command with output to stylize, followed by style options,
+and then the command's normal parameters.
+
+style options include:
+  --bold, --faint, --underline, etc.
+  --color <color_name>
+  --background <color_name>
+  --stderr (output to standard error instead of standard out)
+
+  echo "This is $(style::echo -f --bold LOUD) and soft."
+
+command parameters are those supported by the stylized command.
+
+Use style::stylize --help colors for a list of colors or backgrounds
+Use style::stylize --help attributes for a list of style attribute flags
+
+Usage: style::printf [style options] [command parameters]
+
+style options include:
+  --bold, --faint, --underline, etc.
+  --color <color_name>
+  --background <color_name>
+  --stderr (output to standard error instead of standard out)
+
+  echo "This is $(style::echo -f --bold LOUD) and soft."
+
+command parameters are those supported by the 'printf' command.
+
+Use style::printf --help colors for a list of colors or backgrounds
+Use style::printf --help attributes for a list of style attribute flags
+
+Usage: style::error [style options] [command parameters]
+
+Default style options for style::error:
+  "--stderr --bold --color red"
+
+style options include:
+  --bold, --faint, --underline, etc.
+  --color <color_name>
+  --background <color_name>
+  --stderr (output to standard error instead of standard out)
+
+  echo "This is $(style::echo -f --bold LOUD) and soft."
+
+command parameters are those supported by the echo command.
+
+Use style::error --help colors for a list of colors or backgrounds
+Use style::error --help attributes for a list of style attribute flags
+
+Usage: style::link [style options] [command parameters]
+
+Default style options for style::link:
+  "--underline --color dark_blue"
+
+style options include:
+  --bold, --faint, --underline, etc.
+  --color <color_name>
+  --background <color_name>
+  --stderr (output to standard error instead of standard out)
+
+  echo "This is $(style::echo -f --bold LOUD) and soft."
+
+command parameters are those supported by the echo command.
+
+Use style::link --help colors for a list of colors or backgrounds
+Use style::link --help attributes for a list of style attribute flags
+black
+blue
+cyan
+dark_blue
+dark_cyan
+dark_green
+dark_magenta
+dark_red
+dark_yellow
+default
+gray
+green
+light_gray
+magenta
+pink
+purple
+red
+white
+yellow
+--blink
+--bold
+--faint
+--italic
+--reset
+--underline
+INFO: Info here
+WARNING: Watch out!
+ERROR: What went wrong now?
+WARNING: Customized warning style, still to stderr! :-)
+
+This should still display in bold red, but on stderr
diff --git a/tests/expected/style-tests.out b/tests/expected/style-tests.out
new file mode 100644
index 0000000..87424d7
--- /dev/null
+++ b/tests/expected/style-tests.out
@@ -0,0 +1,39 @@
+------------------
+------------------
+------------------
+----- colors -----
+--- attributes ---
+------------------
+style::echo --bold
+style::echo --bold --color cyan
+style::echo --faint --color green
+style::echo --italic --color magenta
+style::echo --underline --color dark_blue
+style::echo --blink --color light_gray
+style::echo --pink --background dark_cyan
+italic this style may not work in some terminals: style::echo --italic --dark_magenta italic this style may not work in some terminals:
+      Item   Cost
+      ----   ----
+     beans $  2.90
+    franks $  9.35
+      cola $  7.99
+  tiramasu $ 24.50
+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
+
+This will not be styled. It doesn't print directly to the tty
+
+This will not be styled. stderr doesn't print directly to the tty
+STYLE_TO_TTY_ONLY=false
+
+This will be styled even though it doesn't print directly to the tty.
+
+This will be styled even though stderr doesn't print directly to the tty
+This is -f --bold --yellow LOUD and soft.
+This is --force --bold --yellow LOUD and soft.
+This is --tty --bold --yellow LOUD and soft.
+Bad style, Error status: 2
+No orange! Error status: 2
diff --git a/tests/style-test-visually b/tests/style-test-visually
new file mode 100755
index 0000000..876ea64
--- /dev/null
+++ b/tests/style-test-visually
@@ -0,0 +1,87 @@
+#!/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/style.sh
+
+# This is not an automated unit test.
+# It prints stylized text describing the style to be shown, so a tester
+# can validate the expected style is rendered.
+
+# Note that some terminals do not support all terminal styles.
+# For instance, italic may not render as italic on MacOS.
+
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")"/../devshell/lib >/dev/null 2>&1 && pwd)"/style.sh || exit $?
+
+runtest() {
+  command="$1"; shift
+  "${command}" "$@" "${command} $*"
+}
+
+style::stylize --help
+style::echo --blue $'------------------'
+style::printf --help
+style::echo --blue $'------------------'
+style::error --help
+style::echo --blue $'------------------'
+style::link --help
+style::echo --blue $'----- colors -----'
+style::echo --help colors
+style::echo --blue $'--- attributes ---'
+style::echo --help attributes
+style::echo --blue $'------------------'
+
+runtest style::echo --bold
+runtest style::echo --bold --color cyan
+runtest style::echo --faint --color green
+runtest style::echo --italic --color magenta
+runtest style::echo --underline --color dark_blue
+runtest style::echo --blink --color light_gray
+runtest style::echo --pink --background dark_cyan
+runtest style::echo --italic --dark_magenta italic "this style may not work in some terminals:"
+
+style::printf --bold '%10s %6s\n' Item Cost
+style::printf '%10s %6s\n'        ---- ----
+style::printf --purple --background white  '%10s $%6.2f\n' beans 2.90 franks 9.35 cola 7.99 tiramasu 24.50
+
+style::cat --background dark_yellow --black << 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?'
+style::link 'http://wikipedia.com'
+
+STYLE_WARNING='--stderr --blink --dark_yellow'
+style::warning 'WARNING: Customized warning style, still to stderr! :-)'
+
+STYLE_TO_TTY_ONLY=true  # default is false
+style::echo --bold --red "STYLE_TO_TTY_ONLY=$STYLE_TO_TTY_ONLY"
+style::echo --stderr --bold --red '
+This should still display in bold red, but on stderr' >/dev/null
+
+style::echo --color cyan --faint "
+This will not be styled. It doesn't print directly to the tty" | cat
+
+style::echo --stderr --color cyan --faint "
+This will not be styled. stderr doesn't print directly to the tty" 2>&1 | cat
+
+STYLE_TO_TTY_ONLY=false
+style::echo --bold --red "STYLE_TO_TTY_ONLY=$STYLE_TO_TTY_ONLY"
+style::echo --color cyan --faint "
+This will be styled even though it doesn't print directly to the tty." | cat
+
+style::echo --stderr --color cyan --faint "
+This will be styled even though stderr doesn't print directly to the tty" 2>&1 | cat
+
+# Three flags for the same thing:
+echo "This is $(style::echo -f      --bold --yellow LOUD) and soft."
+echo "This is $(style::echo --force --bold --yellow LOUD) and soft."
+echo "This is $(style::echo --tty   --bold --yellow LOUD) and soft."
+
+style::printf --blod --green 'Bad style' 2>/dev/null || echo "Bad style, Error status: $?"
+style::printf --faint --orange 'No orange' 2>/dev/null || echo "No orange! Error status: $?"
diff --git a/tests/style-tests b/tests/style-tests
new file mode 100755
index 0000000..3ddf5da
--- /dev/null
+++ b/tests/style-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/style.sh
+#
+# Usage: style-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}/style-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 $?