Repair broken JCL construction for a few switches

jira-search.sh
--------------
  o Declare more array vars for gathering input.
  o Replace declare with local within functions.
  o Refactored arg detection to set parsting state variables.
  o Replace inline AND/OR construction with list join on and/or.
    Too many edge cases can leave a AND/OR token prefix or suffix
    breaking the JCL query line.
  o do_labels logic updated to handle --label-is-empty and construct
    --excl and --incl arguments as standalone paren wrapped terms.

jira-search/getopt/detect-modifiers.sh
--------------------------------------
  o when -and, -or, -in, -excl, -incl, -is-empty switch modifiers are
    detected set global switch parsing state variables.

Signed-off-by: Joey Armstrong <jarmstrong@linuxfoundation.org>
Change-Id: Ib67bbf1a6389c8d9e9a3ac70911429118228c683
diff --git a/jira/bin/jira-search.sh b/jira/bin/jira-search.sh
index 51959f6..751edb6 100755
--- a/jira/bin/jira-search.sh
+++ b/jira/bin/jira-search.sh
@@ -1,5 +1,5 @@
 #!/bin/bash
-# -----------------------------------------------------------------------
+# -------------------------------------------------------------------------
 # Copyright 2023-2024 Open Networking Foundation Contributors
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -47,6 +47,8 @@
 ##-------------------##
 ##---]  GLOBALS  [---##
 ##-------------------##
+declare -g -a is_empty=()    # label
+
 declare -g -a text=()
 declare -g -a text_and=()
 declare -g -a text_or=()
@@ -60,10 +62,8 @@
 declare -g -a projects=()
 
 path="$(realpath $0 --canonicalize-existing)"
-# source "${path%\.sh}/utils.sh"
 source "${pgm_lib}/utils.sh"
-#source "$pgmlib/fixversion.sh"
-#source "$pgmlib/resolved.sh"
+source "${pgm_lib}/include.sh"
 source "${pgm_lib}/fixversion.sh"
 source "${pgm_lib}/resolved.sh"
 source "${pgm_lib}/help/utils.sh"
@@ -79,10 +79,43 @@
 ** ERROR: $@
 ** -----------------------------------------------------------------------
 ERROR
-    echo 
+    echo
     exit 1
 }
 
+## --------------------------------------------------------------------
+## --------------------------------------------------------------------
+function banner()
+{
+    cat <<MSG
+
+** -----------------------------------------------------------------------
+** $@
+** -----------------------------------------------------------------------
+MSG
+    return
+}
+
+## -----------------------------------------------------------------------
+## Intent: Append a conditional token in the list based on context
+## -----------------------------------------------------------------------
+function and_or()
+{
+    local -n ref=$1; shift
+    local val="$1" ; shift
+
+    if [[ ${#ref[@]} -gt 0 ]]; then
+        if [[ -v bool_or ]]; then
+            ref+=('OR')
+        else
+            ref+=('AND')
+        fi
+    fi
+
+    ref+=("$val")
+    return
+}
+
 ## -----------------------------------------------------------------------
 ## -----------------------------------------------------------------------
 function html_encode()
@@ -165,23 +198,31 @@
 ## Intent: Query filter by labels assigned to a ticket:
 ##   o pods, failing, testing
 ## --------------------------------------------------------------------
+## Perform a few string joins
+##   - elements {in,not-in} label {excl, incl}
+##   -  OR: is-empty + in-label
+##   - AND: excl + incl
+## --------------------------------------------------------------------
 # "project in (UKSCR, COMPRG) AND issuetype = Bug AND labels in (BAT)" and
 ## --------------------------------------------------------------------
 function do_labels()
 {
-    declare -n incl=$1; shift # was args=
-    declare -n excl=$1; shift
-    declare -n ans=$1; shift
+    local -n incl=$1; shift # was args=
+    local -n excl=$1; shift
+    local -n ans=$1; shift
 
-    ## --------------------------------
-    ## Conjunction if stream tokens > 0
-    ## --------------------------------
-    conjunction ans
+    ## -------------------------------
+    ## Join #1: is-empty + labels-incl
+    ## -------------------------------
+    local -a tokens=()
 
-    declare -a tokens=()
+    if [[ " ${is_empty[*]} " =~ ' label ' ]]; then
+        tokens+=('(labels IS EMPTY)')
+    fi
 
-    ## -----------------------------
-    ## -----------------------------
+    ## ------------------------------
+    ## Construct label include filter
+    ## ------------------------------
     if [[ ${#incl[@]} -gt 0 ]]; then
 
         local modifier
@@ -192,25 +233,33 @@
         fi
 
         local labels=$(join_by ',' "${incl[@]}")
-        local -a tmp=(\
-                      '('\
-                          'label IS EMPTY' \
-                          'OR' \
-                          "labels ${modifier} ($labels)" \
-                          ')'\
-            )
-        tokens+=("${tmp[@]}")
+        tokens+=("(labels ${modifier} ($labels))")
     fi
 
-    conjunction tokens 'AND'
+    # ------------------------------
+    # JOIN[OR]: is-empty + in-labels
+    # ------------------------------
+    if [[ ${#tokens[@]} -gt 1 ]]; then
+        local combine
+        combine=("$(join_by ' OR ' "${tokens[@]}")")
+        tokens=("$combine")
+    fi
 
-    ## -----------------------------
-    ## -----------------------------
+    ## ------------------------------
+    ## Construct label exclude filter
+    ## ------------------------------
     if [[ ${#excl[@]} -gt 0 ]]; then
         local labels=$(join_by ',' "${excl[@]}")
-        tokens+=('(' "labels NOT IN ($labels)" ')')
+        tokens+=("(labels NOT IN ($labels))")
     fi
 
+    # ------------------------------------
+    # JOIN[AND]: labels-excl + labels-incl
+    # ------------------------------------
+    if [[ ${#tokens[@]} -gt 1 ]]; then
+        tokens=("$(join_by ' AND ' "${tokens[@]}")")
+    fi
+    
     ans+=("${tokens[@]}")
     return
 }
@@ -220,19 +269,19 @@
 ## --------------------------------------------------------------------
 function do_projects()
 {
-    declare -n ref=$1; shift
+    local -n ref=$1; shift
 
     [[ ${#projects[@]} -eq 0 ]] && { return; }
 
     local terms="$(join_by ',' "${projects[@]}")"
-#    local -a buffer=('(' 'project' 'IN' "($terms)" ')')
-#    ref+=("$(join_by '%20' "${buffer[@]}")")
+    #    local -a buffer=('(' 'project' 'IN' "($terms)" ')')
+    #    ref+=("$(join_by '%20' "${buffer[@]}")")
     ref+=("(project IN ($terms))")
     return
 }
 
 ## --------------------------------------------------------------------
-## Intent: Query by compound text filters
+## Intent: Construct query using text field filters
 ## --------------------------------------------------------------------
 function do_text()
 {
@@ -252,7 +301,7 @@
 
     ## Append terms: AND
     if [[ ${#text_and[@]} -gt 0 ]]; then
-        declare -a term=()
+        local -a term=()
         for val in "${text_and[@]}";
         do
             term+=("text ~ \"$val\"")
@@ -263,7 +312,7 @@
 
     ## Append terms: OR
     if [[ ${#text_or[@]} -gt 0 ]]; then
-        declare -a term=()
+        local -a term=()
         for val in "${text_or[@]}";
         do
             term+=("text ~ \"$val\"")
@@ -282,7 +331,7 @@
 ## --------------------------------------------------------------------
 function do_user()
 {
-    declare -n ans=$1; shift
+    local -n ans=$1; shift
 
     [[ -v argv_nobody ]] && return
 
@@ -307,22 +356,34 @@
 ## --------------------------------------------------------------------
 function gen_filter()
 {
-    declare -n ans=$1; shift
-    declare -n args=$1; shift
+    local -n ans=$1  ; shift
+    local -n args=$1 ; shift
 
     ## -----------------------------------
     ## Begin by joining major search terms
     ## -----------------------------------
-    declare -a _tmp=()
+    local -a _tmp=()
     local val
+
+    local -i is_paren=0
+    local -a buffer=()
     for val in "${args[@]}";
     do
-        _tmp+=("$val" 'AND')
+        case "$val" in
+            '(') _tmp+=("$val"); continue ;;
+            ')') _tmp+=("$val"); continue ;;
+        esac
+
+        and_or _tmp "$val"
     done
 
-    if [[ ${#_tmp[@]} -gt 0 ]]; then
-        unset _tmp[-1]
-    fi
+    ## ----------------------------------------------------------------
+    # This was used to remove AND term when a non-query or non-argument
+    # query was needed.  Any lingering logic dependent on it ?
+    ## ----------------------------------------------------------------
+    #    if [[ ${#_tmp[@]} -gt 0 ]]; then
+    #        unset _tmp[-1]
+    #    fi
 
     ## -----------------------
     ## Massage with html codes
@@ -336,11 +397,11 @@
 ## --------------------------------------------------------------------
 function gen_url()
 {
-    declare -n ans=$1; shift
-    declare -n args=$1; shift
+    local -n ans=$1; shift
+    local -n args=$1; shift
 
     ## Which jira server to query (?)
-    [[ ! -v server ]] && declare -g server='jira.opennetworking.org'
+    [[ ! -v server ]] && local -g server='jira.opennetworking.org'
     tmp_url="https://${server}/issues/?jql="
     tmp="${tmp_url}${args}"
     ans="${tmp// /%20}"
@@ -371,16 +432,16 @@
   --text        Search string(s)
 EOH
 
-#    declare -a topics=()
-#    topics+=('fixversion.switches')
-#    topics+=('resolved.switches')
-#
-#    help_switch_show "${topics[@]}"#
+    #    local -a topics=()
+    #    topics+=('fixversion.switches')
+    #    topics+=('resolved.switches')
     #
-#[USER(s)]
-#  --me          Tickets assigned to or reported by me.
-#  --user [u]    Tickets assigned to this user.
-#  --nobody      Raw query, no filtering by user
+    #    help_switch_show "${topics[@]}"#
+    #
+    #[USER(s)]
+    #  --me          Tickets assigned to or reported by me.
+    #  --user [u]    Tickets assigned to this user.
+    #  --nobody      Raw query, no filtering by user
 
 
     cat <<EOH
@@ -393,10 +454,34 @@
   --in             (default) Items belong (--component IN)
   --not-in         Negate item set (--component NOT IN)
 
-[Contains]
-  --text     [t]   (join modifer: --and, --or)
-  --text-and [t]   All of these terms
-  --text-or  [t]   Any of these terms
+[Contains] (join modifer: --and, --or, --is-empty)
+  --text        [t]
+  --text-and    [t]   All list items
+  --text-or     [t]   Any list items
+  --label       [l]   Match label field based on modifier criteria
+  --text-and    [t]   All list items
+  --text-or     [t]   Any list items
+
+[MODIFIERS]
+  --and                Combine query terms using AND keyword
+  --or                 Combine query terms using OR  keyword
+  --in                 Include ticket field if list item(s) match
+  --not                Negate a query
+  --is-empty           Include ticket if field is empty
+  --resolved-is-empty
+  --unresolved         Query for open/unresolved jira tickets.
+
+[FILTER]
+  --excl           Exclude tickets whose fields match this string (filter-out)
+  --incl           Include tickets whose fields match this string
+
+[--resolved]
+  --resolved-is-empty
+  --resolved-start               Query by date range
+  --resolved-end                 Query by date range
+  --resolved(-not)-empty         Query for (closed)/open tickets
+  --resolved-excl                Types to exclude
+  --resolved-incl                Types to include
 
 [RANGE]
   --newer [d]      Search for tickets created < [n] days ago.
@@ -404,6 +489,8 @@
 
 [ALIASES]
   --all            Query for all unresolved tickets
+  --unresolved     Alias for --resolved-is-empty
+  --wip            Alias for --resolved-is-empty
 
 [TOPIC]
   --fixversion     Query by field: fixedversion
@@ -423,8 +510,6 @@
      o Search for tickets that contain strings bbsim or release
   $0 --cord --text-and 'release' --text-and 'voltctl'
      o Search jira.opencord for tickets that contain release and voltctl
-  $0 --text 'bitergia' --text 'Jira' -and
-     o Search jira.opennetworking for tickets containing string bitergia and Jira
 
   $0 --cord --label failing --label pod
      o Search jira.opencord for tests failing due to pod/hardware issuses.
@@ -447,26 +532,54 @@
 ##----------------##
 ##---]  MAIN  [---##
 ##----------------##
-declare -a suffix0=()
+declare -a suffix0=()               # accumulated terms to join
+declare server='jira.opencord.org'
 
 # declare -g -i debug=1
 
 while [ $# -gt 0 ]; do
 
-    if [ ${#suffix0[@]} -gt 0 ]; then
-        suffix0+=('AND')
-    fi
+#    if [ ${#suffix0[@]} -gt 0 ]; then
+#        suffix0+=('AND')
+#    fi
 
     arg="$1"; shift
+    banner "ARG=[$arg], \$@=[$@]"
+    
     [[ -v debug ]] && echo "** argv=[$arg] [$*]"
 
+    getopt_detect_modifiers "$arg"
+
     case "$arg" in
 
-        '--help') usage; exit 0 ;;
-        '--help-'*) help_with "${arg/--help-/}" ;;
-        '--usage-'*) help_usage_show "${arg/--usage-/}"
-           ;;
+        '--help')    usage; exit 0                      ;;
+        '--help-'*)  help_with "${arg/--help-/}"        ;;
+        '--usage-'*) help_usage_show "${arg/--usage-/}" ;;
 
+        '--'*'-is-empty')
+            declare -a args=()
+            args+=('--is-empty')
+
+            arg="${arg:2}"           # remove prefix --
+            arg="${arg%-is-empty}"   # remove suffix token-name
+            args+=("$arg")
+            [[ $# -gt 0 ]] && { args+=("$@"); }
+
+            set -- "${args[@]}"
+            ;;
+        
+        '--is-empty')
+            declare val="$1"; shift
+            declare -a valid=()
+            valid+=('label')
+
+            if [[ " ${valid[@]} " =~ " ${val} " ]]; then
+                is_empty+=("$val")
+            else
+                error "Detected invalid --is-empty switch [$arg]"
+            fi
+        ;;
+        
         ##-----------------##
         ##---]  MODES  [---##
         ##-----------------##
@@ -476,7 +589,7 @@
         ##------------------------##
         ##---]  SWITCH ALIAS  [---##
         ##------------------------##
-        --unresolved)
+        --unresolved|--wip)
             declare -a args=()
             args+=('--resolved-is-empty')
             [[ $# -gt 0 ]] && { args+=("$@"); }
@@ -502,12 +615,12 @@
             arg="$1"; shift
             case "$arg" in
                 *cord*) server='jira.opencord.org' ;;
-                 *onf*) server='jira.opennetworking.org' ;;
-                 *) error "--server [$arg] expected opencord or onf" ;;
+                *onf*) server='jira.opennetworking.org' ;;
+                *) error "--server [$arg] expected opencord or onf" ;;
             esac
             ;;
 
-             --onf) declare server='jira.opennetworking.org' ;;
+        --onf) declare server='jira.opennetworking.org' ;;
         --opencord) declare server='jira.opencord.org'     ;;
 
         ##---------------------##
@@ -519,35 +632,46 @@
             components+=("$arg")
             ;;
 
-        --label-excl)
-            arg="$1"; shift
-            labels_excl+=("$arg")
-            ;;
-
-        --label|--label-incl)
-            arg="$1"; shift
-            labels_incl+=("$arg")
+        '--label-is-empty') is_empty+=('label') ;;
+            
+        '--label'|'--lab'*)
+            val="$1"; shift
+            if [[ -v getopt_argv_EXCL ]]; then
+                labels_excl+=("$val")
+            elif [[ -v getopt_argv_INCL ]]; then
+                labels_incl+=("$val")
+            else
+                labels_incl+=("$val")
+            fi
             ;;
 
         ##-----------------------##
         ##---]  Text Search  [---##
         ##-----------------------##
         # jsearch.sh --text-and bbsim --text-and release
-        -*text-and) text_and+=("$1"); shift ;;
-        -*text-or) text_or+=("$1");   shift ;;
+        '--tex'*)
+            [[ $# -gt 0 ]] && { val="$1"; }
+            case "$arg" in
+                '--text-and') text_and+=("$val") ;;
+                '--text-or')  text_or+=("$val")  ;;
 
-        # % js --and --text jenkins --text cord
-        # text ~ "Jira Software"
     # [WORDs]
-        # text ~ "\"Jira Software\""
 # [STRING]
-        -*text)
-            arg="$1"; shift
-            if [[ -v bool_and ]]; then
-                text_and+=("$arg")
-            elif [[ -v bool_or ]]; then
-                text_or+=("$arg")
-            else
-                text+=("$arg")
-            fi
+                # % js --and --text jenkins --text cord
+                # text ~ "Jira Software"      # [WORDs]
+                # text ~ "\"Jira Software\""  # [STRING]
+                '--text')
+                    if [[ ! -v bool_and_or ]]; then
+                        error "Qualify [$arg] using --text-{and,or}"
+                    elif [[ bool_and_or -eq 1 ]]; then
+                        text_and+=("$1");  shift
+                    elif [[ bool_and_or -eq 0 ]]; then
+                        text_or+=("$1");  shift
+                    else
+                        error "Qualify [$arg] using --text-{and,or}"
+                    fi
+                    ;;
+                *) error "Qualify [$arg] using --text-{and,or}" ;;
+            esac
+            shift # $val
             ;;
 
         --all) set -- '--resolved-is-none' "$@" ;; # alias: --[un-]resolved
@@ -558,24 +682,24 @@
         --fixversion-*)
             # function get_jql_fixversion()
             case "$arg" in
-                  *excl)
-                      [[ ! -v fixversion_excl ]] && { declare -g -a fixversion_excl=(); }
-                      val="\"$1\""; shift
-                      html_encode val
-                      fixversion_excl+=("$val");
-                      ;;
+                *excl)
+                    [[ ! -v fixversion_excl ]] && { declare -g -a fixversion_excl=(); }
+                    val="\"$1\""; shift
+                    html_encode val
+                    fixversion_excl+=("$val");
+                    ;;
 
-                  *incl)
-                      [[ ! -v fixversion_incl ]] && { declare -g -a fixversion_incl=(); }
-                      val="\"$1\""; shift
-                      html_encode val
-                      fixversion_incl+=("$val");
-                      ;;
+                *incl)
+                    [[ ! -v fixversion_incl ]] && { declare -g -a fixversion_incl=(); }
+                    val="\"$1\""; shift
+                    html_encode val
+                    fixversion_incl+=("$val");
+                    ;;
 
-                  *not-empty) declare -g -i fixversion_not_empty=1 ;;
-                   *is-empty) declare -g -i fixversion_is_empty=1  ;;
+                *not-empty) declare -g -i fixversion_not_empty=1 ;;
+                *is-empty) declare -g -i fixversion_is_empty=1  ;;
 
-                  *) error "Detected invalid --fixversion-* modifier" ;;
+                *) error "Detected invalid --fixversion-* modifier" ;;
             esac
             ;;
 
@@ -584,25 +708,25 @@
             case "$arg" in
 
                 *start) declare -g resolved_start="$1"; shift ;;
-                  *end) declare -g resolved_end="$1";   shift ;;
+                *end) declare -g resolved_end="$1";   shift ;;
 
                 *not-empty) declare -g resolved_not_empty="$1" ;;
-                    *empty) declare -g resolved_is_empty="$1"  ;;
+                *empty) declare -g resolved_is_empty="$1"  ;;
 
-                  *excl)
-                      [[ ! -v resolved_excl ]] && { declare -g -a resolved_excl=(); }
-                      val="\"$1\""; shift
-                      html_encode val
-                      resolved_excl+=("$val");
-                      ;;
-                  *incl)
-                      [[ ! -v resolved_incl ]] && { declare -g -a resolved_incl=(); }
-                      val="\"$1\""; shift
-                      html_encode val
-                      resolved_incl+=("$val");
-                      ;;
-                 *) ;;
-                     *) error "Detected invalid --resolved-* modifier" ;;
+                *excl)
+                    [[ ! -v resolved_excl ]] && { declare -g -a resolved_excl=(); }
+                    val="\"$1\""; shift
+                    html_encode val
+                    resolved_excl+=("$val");
+                    ;;
+                *incl)
+                    [[ ! -v resolved_incl ]] && { declare -g -a resolved_incl=(); }
+                    val="\"$1\""; shift
+                    html_encode val
+                    resolved_incl+=("$val");
+                    ;;
+                *) ;;
+                *) error "Detected invalid --resolved-* modifier" ;;
             esac
             ;;
 
@@ -617,8 +741,14 @@
         ##----------------##
         ##---]  BOOL  [---##
         ##----------------##
-        --[aA][nN][dD]) declare -g -i bool_and=1 ;;
-        --[oO][rR])     declare -g -i bool_or=1  ;;
+        --[aA][nN][dD])
+            declare -g -i bool_and=1
+            declare -g -i bool_and_or=1 # ! -v else toggle
+            ;;
+        --[oO][rR])
+            declare -g -i bool_or=1
+            declare -g -i bool_and_or=0
+            ;;
 
         ##------------------##
         ##---]  MEMBER  [---##
@@ -626,7 +756,7 @@
         --[iI][nN])     declare -g -i bool_in=1  ;;
         --[nN][oO][tT]) declare -g -i bool_not=1 ;;
 
-        [A-Z][A-Z][A-Z]-[0-9]*)
+        [A-Z][A-Z][A-Z]-[0-9]*) # VOL-xxxx (jira ticket)
             case "$arg" in
                 CORD-[0-9]*)
                     url="https://jira.opencord.org/browse/${arg}"
@@ -648,6 +778,23 @@
             esac
             ;;
 
+        ## ---------------------------
+        ## Search all fields for value
+        ## ---------------------------
+        [[:word:]]*)
+
+            echo "MATCHED: [$arg] (LINENO: $LINENO)"
+            declare -a args=()
+            args+=('--OR')
+            args+=('--component'  "$arg")
+            args+=('--label-incl' "$arg")
+            args+=('--text-or'    "$arg")
+            [[ $# -gt 0 ]] && { args+=("$@"); }
+            set -- "${args[@]}"
+            ;;
+
+        -*) error "Detected unknown switch [$arg]" ;;
+
         # -----------------------------------------------------------------------
         # https://support.atlassian.com/jira-software-cloud/docs/search-syntax-for-text-fields/
         # -----------------------------------------------------------------------
@@ -655,10 +802,12 @@
         # -japan          -- exclude term
         # [STEM] summary ~ "customize"    -- finds stem 'custom' in the Summary field
         *)
+            echo "MATCHED: [$arg] (LINENO: $LINENO)"
             declare -p text_and
             error "Detected unknown argument $arg"
             ;;
     esac
+
 done
 
 ## --------------
@@ -677,6 +826,20 @@
 do_resolved                       suffix0
 do_fixversion                     suffix0
 
+declare -p suffix0
+
+if [[ -v getopt_argv_AND ]]; then
+    query="$(join_by 'AND' "${suffix0[0]}")"
+elif [[ -v getopt_argv_OR ]]; then
+    query="$(join_by 'OR' "${suffix0[0]}")"
+else
+    error "Ambiguous query [argv needs: --and or --or]"
+fi
+
+# banner "$(declare -p query)"
+
+
+
 filter=''
 gen_filter filter suffix0
 
diff --git a/jira/jira-search/getopt/detect-modifiers.sh b/jira/jira-search/getopt/detect-modifiers.sh
new file mode 100644
index 0000000..84a1afd
--- /dev/null
+++ b/jira/jira-search/getopt/detect-modifiers.sh
@@ -0,0 +1,95 @@
+#!/bin/bash
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
+
+## -----------------------------------------------------------------------
+## Intent: Set state flags for parsing based on detection of switch modifiers
+## -----------------------------------------------------------------------
+function getopt_detect_modifiers()
+{
+    local arg="$1"; shift
+
+    local -a patterns=()
+    patterns+=('and'  'or')
+    patterns+=('excl' 'incl')
+    patterns+=('in'   'not')
+    patterns+=('is-empty')
+
+    unset getopt_argv_AND
+    unset getopt_argv_EXCL
+    unset getopt_argv_INCL
+    unset getopt_argv_IN
+    unset getopt_argv_IS_EMPTY
+    unset getopt_argv_NOT
+    unset getopt_argv_OR
+
+    local pattern
+    for pattern in "${patterns[@]}";
+    do
+        # echo "** ${FUNCNAME} pattern=[$pattern], arg=[$arg]"
+        case "$pattern" in
+            'and')
+                case "$arg" in
+                    *'-'[aA][nN][dD]*)        declare -g -i getopt_argv_AND=1 ;;
+                esac
+                ;;
+
+            'or')
+                case "$arg" in
+                    *'-'[oO][rR]*)
+                        declare -g -i getopt_argv_OR=1
+                        ;;
+                esac
+                ;;
+
+            'excl')
+                case "$arg" in                    
+                    *'-'[eE][xX][cC][lL]*)    declare -g -i getopt_argv_EXCL=1 ;;
+                esac
+                ;;
+            
+            'incl')
+                case "$arg" in                    
+                    *'-'[iI][nN][cC][lL]*)    declare -g -i getopt_argv_INCL=1 ;;
+                esac
+                ;;
+
+            'in')
+                if [[ ! -v getopt_argv_INCL ]]; then
+                    case "$arg" in                    
+                        *'-'[iI][nN]*)            declare -g -i getopt_argv_IN=1 ;;
+                    esac
+                fi
+                ;;
+
+            'is-empty')
+                case "$arg" in                    
+                    *'-'[iI][sS]-[eE][mM][pP][tT][yY])  declare -g -i getopt_argv_IS_EMPTY=1 ;;
+                esac
+                ;;
+
+            'not')
+                case "$arg" in                    
+                    *'-'[nN][oO][tT]*)        declare -g -i getopt_argv_NOT=1 ;;
+                esac
+                ;;
+
+        esac
+        
+    done
+
+    if false; then
+        [[ -v getopt_argv_AND ]] && { declare -p getopt_argv_AND; }
+        [[ -v getopt_argv_EXCL ]] && { declare -p getopt_argv_EXCL; }
+        [[ -v getopt_argv_INCL ]] && { declare -p getopt_argv_INCL; }
+        [[ -v getopt_argv_IN ]] && { declare -p getopt_argv_IN; }
+        [[ -v getopt_argv_IS_EMPTY ]] && { declare -p getopt_argv_IS_EMPTY; }
+        [[ -v getopt_argv_NOT ]] && { declare -p getopt_argv_NOT; }
+        [[ -v getopt_argv_OR ]] && { declare -p getopt_argv_OR; }
+    fi
+    
+    : # return $?==0
+    return
+}
+
+# [EOF]
diff --git a/jira/jira-search/include.sh b/jira/jira-search/include.sh
new file mode 100644
index 0000000..57405c9
--- /dev/null
+++ b/jira/jira-search/include.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
+
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
+function init()
+{
+    declare pgm=''
+    pgm="$(realpath --canonicalize-existing "$0")"
+    readonly pgm
+
+    declare -g pgmsrc
+    pgmsrc="$(readlink --canonicalize-existing "${BASH_SOURCE[0]}")"
+    readonly pgmsrc
+
+    # stack-trace-on-error
+    # interrupt handler
+    # mkdir with auto-cleanup at exit
+    declare pgm_root="${pgm%%/jira/jira-search/include.sh}"
+
+    declare root
+    root="${pgm%%/jira/bin/jira-search.sh}"
+    source "$root/lf/onf-common/common.sh" '--common-args-begin--'
+
+    pgm_lib="${root}/jira/jira-search"
+    readonly pgm_lib
+
+    pgm_bin="${root}/bin"
+    readonly pgm_bin
+
+    pgm_help="${root}/jira/jira-search/help"
+    readonly pgm_help
+}
+# init
+# unset init
+
+##--------------------##
+##---]  INCLUDES  [---##
+##--------------------##
+source "${pgm_lib}/getopt/detect-modifiers.sh"
+
+# source "${pgm_lib}/utils.sh"
+#source "$pgmlib/fixversion.sh"
+#source "$pgmlib/resolved.sh"
+source "${pgm_lib}/fixversion.sh"
+source "${pgm_lib}/resolved.sh"
+source "${pgm_lib}/help/utils.sh"
+
+# [EOF]
diff --git a/jira/makefile b/jira/makefile
index 02f5ad5..ab4c3eb 100644
--- a/jira/makefile
+++ b/jira/makefile
@@ -1,8 +1,37 @@
 # -*- makefile -*-
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
 
+jira-search	= bin/jira-search.sh
+
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
 all:
 
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
 view :
 	pandoc README.md | lynx --stdin
 
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
+query-args	:= $(null)
+# query-args	+= --dry-run
+query-args	+= --opencord
+query-args	+= --or
+query-args	+= --label-is-empty
+query-args	+= --label   'python'
+query-args	+= --label-excl 'foobar'
+query-args	+= --text-or 'python'
+
+query :
+	$(jira-search) $(query-args) 2>&1 | tee log
+
+## -----------------------------------------------------------------------
+## -----------------------------------------------------------------------
+help :
+	@printf 'Usage: make [options] [target] ...'
+	@printf '  %-33.33s %s\n' 'query'  'Convenience target for jira queries'
+	@printf '  %-33.33s %s\n' 'view'   'Render README.md for local viewing'
+
 # [EOF]