#!/bin/bash
#==============================================================================
#==============================================================================
# Declarations and Environment
export TOOLS_DIR="${TOOLS_DIR:-$PWD}"
# Environment isolation. For stability and security reasons, prepend
# PATH to include dirs where known-good scripts exist.
# known/tested PATH and, by implication, executables on the PATH.
export PATH=$TOOLS_DIR:$PATH:.
export P4CONFIG=${P4CONFIG:-.p4config}
declare ThisScript="${0##*/}"
declare Version="1.7.0.J2J.1"
declare ReportStyle="OpenJobs"
declare ReportStyleTitle=
declare JobSearchQuery=
declare WorkshopUser="Unset"
declare JobStatusValues=
declare MaxTitleLength=70
declare Status=
declare Type=
declare Priority=
declare OwnedBy=
declare ReportedBy=
declare FullDescription=
declare DevNotes=
declare Title=
declare -i ShowUserJobsOnly=0
declare -i SilentMode=0
declare -i JobCount=0
declare -i ErrorCount=0
declare -i LogCounter=1
declare -i Debug=${J2J_DEBUG:-0}
declare Log=
declare LogLink="${ThisScript%.sh}.log"
declare -i UserLogSet=0
declare -i GenCSVFile=0
declare CSVFile=
declare Verbosity=3
declare H="=============================================================================="
declare TmpFile=
# Jobs data indexed by job name.
declare -A JobType
declare -A JobStatus
declare -A JobPriority
declare -A JobOwnedBy
declare -A JobFullDescription
declare -A JobReportedBy
declare -A JobDevNotes
declare -A JobTitle
declare -A JobTitle
#==============================================================================
# Local Functions
function msg () { echo -e "$*"; }
# shellcheck disable=SC2317
function dbg () { [[ "$Debug" -eq 0 ]] || msg "DEBUG: $*"; }
function vmsg () { [[ "$Verbosity" -ge 4 ]] || return; msg "$*"; }
function errmsg () { msg "\\nError: ${1:-Unknown Error}\\n"; ErrorCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "$ErrorCount"; }
#------------------------------------------------------------------------------
# Function: terminate
# shellcheck disable=SC2317
function terminate
{
# Disable signal trapping.
trap - EXIT SIGINT SIGTERM
dbg "$ThisScript: EXITCODE: $ErrorCount"
# Stop logging.
[[ "${Log}" == off ]] || msg "\\nLog is: $Log\\n${H}"
# With the trap removed, exit.
exit "$ErrorCount"
}
#------------------------------------------------------------------------------
# Function: usage (required function)
#
# Input:
# $1 - style, either -h (for short form) or -man (for man-page like format).
# The default is -h.
#
# $2 - error message (optional). Specify this if usage() is called due to
# user error, in which case the given message displayed first, followed by the
# standard usage message (short or long depending on $1). If displaying an
# error, usually $1 should be -h so that the longer usage message doesn't
# obscure the error message.
#
# Sample Usage:
# usage
# usage -h
# usage -man
# usage -h "Incorrect command line usage."
#------------------------------------------------------------------------------
function usage
{
declare style=${1:--h}
declare errorMessage=${2:-Unset}
if [[ $errorMessage != Unset ]]; then
msg "\\n\\nUsage Error:\\n\\n$errorMessage\\n\\n"
fi
msg "USAGE for $ThisScript v$Version:
$ThisScript [-a|-o] [-me | -u <workshop_user>] [-st <status1>[,<status2>,...]] [-csv] [-L <log>] [-si] [-v<n>] [-n] [-d|-D]
or
$ThisScript [-h|-man]
"
if [[ $style == -man ]]; then
echo -e "
DESCRIPTION:
This script generates a report of J2J jobs.
OPTIONS:
-a Show all J2J jobs regardless of the state.
-o Show only \"Open\" J2J jobs, which includes jobs in these states:
* open
* inprogress
* blocked
This skips jobs in these states:
* closed
* duplicate
* fixed (optional status to use before closed).
* punted
* obsolete
* suspended
The '-o' behaviour is the default.
-me Show only jobs for which the OwnedBy setting is the current user.
Guessing logic is applied to map the current OS user to a Workshop account,
e.g. ttyler -> tom_tyler, etc. This works for some J2J project members.
See the guess_workshop_user() function in env.sh for details.
Works with '-a' and '-o'.
-u <workshop_user>
Show only jobs for which the OwnedBy setting is the specified Workshop user
account.
Specify the special value 'none' to list unassigned jobs, i.e. those for
which the OwnedBy field is not set.
Works with '-a' and '-o'.
-st <status1>[,<status2>,...]
Specify a comma-delimited list of status values to include.
The valid values can be seen by doing: p4 jobspec -o
-csv Generate a CSV file form of the report in addition to the standard report.
This is intended to support JIRA import.
-v<n> Set verbosity 1-5 (-v1 = quiet, -v5 = highest).
The default is -v3.
Specify -v4 to see the query used with the 'p4 jobs -e' command.
-L <log>
Specify the path to report log file, or the special value 'off' to disable
logging. By default, all output (stdout and stderr) goes to a file, then
name of which is displayed when the script starts.
NOTE: This script is self-logging. That is, output displayed on the screen
is simultaneously captured in the log file. Do not run this script with
redirection operators like '> log' or '2>&1', and do not use 'tee.'
In addition to the log file, this script updates a log symlink with a
fixed name, $LogLink, that points to the unique log file name.
-si Operate silently. All output (stdout and stderr) is redirected to the log
only; no output appears on the terminal. This cannot be used with
'-L off'.
-n No-Op. Prints commands instead of running them.
-d Set debugging verbosity.
-D Set extreme debugging verbosity with bash 'set -x' mode. Implies '-d'.
HELP OPTIONS:
-h Display short help message
-man Display man-style help message
EXAMPLES:
Typical usage is with no arguments, to display open jobs:
$ThisScript
Show opened jobs assigned to me (based on the \$WORKSHOP_USER shell
environment setting).
$ThisScript -me
Show only items listed as inprogress:
$ThisScript -st inprogress
"
fi
exit 2
}
#==============================================================================
# Command Line Processing
declare -i shiftArgs=0
set +u
while [[ $# -gt 0 ]]; do
case $1 in
(-a) ReportStyle="AllJobs";;
(-o) ReportStyle="OpenJobs";;
(-me) ShowUserJobsOnly=1; WorkshopUser="me";;
(-u) ShowUserJobsOnly=1; WorkshopUser="$2"; shiftArgs=1;;
(-st) JobStatusValues="$2"; shiftArgs=1;;
(-csv) GenCSVFile=1;;
(-h) usage -h;;
(-man) usage -man;;
(-v1) Verbosity=1;;
(-v2) Verbosity=2;;
(-v3) Verbosity=3;;
(-v4) Verbosity=4;;
(-v5) Verbosity=5;;
(-L) Log="$2"; UserLogSet=1; shiftArgs=1;;
(-si) SilentMode=1;;
(-n) export NO_OP=1;;
(-d) Debug=1;;
(-D) set -x; Debug=1;; # Debug; use 'set -x' mode.
(*) usage -h "Unknown arg ($1).";;
esac
# Shift (modify $#) the appropriate number of times.
shift; while [[ $shiftArgs -gt 0 ]]; do
[[ $# -eq 0 ]] && usage -h "Incorrect number of arguments."
shiftArgs=$shiftArgs-1
shift
done
done
set -u
#==============================================================================
# Command Line Verification
# shellcheck disable=SC1090 disable=SC1091
source "$TOOLS_DIR/env.sh"
[[ $SilentMode -eq 1 && "$Log" == off ]] && \
usage -h "Cannot use '-si' with '-L off'."
[[ -n "$Log" ]] || Log="$PWD/jobs_report.${WORKSHOP_PROJECT_TAG}.$(date +'%Y%m%d-%H%M').txt"
[[ "$WorkshopUser" == "me" ]] && WorkshopUser="$(guess_workshop_user)"
#==============================================================================
# Main Program
trap terminate EXIT SIGINT SIGTERM
if [[ "$Log" != off ]]; then
# Initialize a unique log file. If the user specified a non-default log name with
# '-L', move any existing log by that name aside so the new log will have the
# intended name. If the log name is the default timestamp-based log file name,
# and that file already exists (e.g. due to concurrent operations), increment a
# counter baked into the name until we find an available log name to use.
if [[ "$UserLogSet" -eq 1 ]]; then
if [[ -e "$Log" ]]; then
MovedLog="${Log%.log}.moved.$LogCounter.$$.log"
while [[ -e "$MovedLog" ]]; do
LogCounter+=1
MovedLog="${Log%.log}.moved.$LogCounter.$$.log"
done
fi
else
while [[ -e "$Log" ]]; do
Log="$PWD/jobs_report.${WORKSHOP_PROJECT_TAG}.$(date +'%Y%m%d-%H%M').${LogCounter}.$$.txt"
LogCounter+=1
done
fi
touch "$Log" || bail "Couldn't touch log file [$Log]."
# Target the fixed-name log symlink to the unique log file.
if [[ -L "$LogLink" ]]; then
rm -f "$LogLink" || bail "Could not do: rm -f \"$LogLink\""
elif [[ -e "$LogLink" ]]; then
mv -f "$LogLink" "$LogLink.moved.$(date +'%Y%m%d-%H%M').log" ||\
bail "Could not do this to move this file to make way for a symlink: mv -v \"$LogLink\""
fi
ln -s "${Log##*/}" "$LogLink" || bail "Couldn't do: ln -s \"${Log##*/}\" \"$LogLink\""
# Redirect stdout and stderr to a log file.
if [[ $SilentMode -eq 0 ]]; then
exec > >(tee "$Log")
exec 2>&1
else
exec >"$Log"
exec 2>&1
fi
msg "${H}\\nLog is: $Log"
CSVFile="${Log%.txt}.csv"
else
CSVFile="${LogLink%.log}.csv"
fi
msg "Started $ThisScript v$Version as $(id -u -n)@$(hostname -s) on $(date)."
if [[ $ReportStyle == "OpenJobs" ]]; then
ReportStyleTitle="Open Jobs"
if [[ -n "$JobStatusValues" ]]; then
# Query to show only J2J jobs in specific states.
if [[ "$JobStatusValues" =~ /,/ ]];then
# Query for a single job status
JobSearchQuery="Project=$WORKSHOP_PROJECT Status=$JobStatusValues"
else
# Query for multiple job status values
JobSearchQuery="Project=$WORKSHOP_PROJECT ("
declare -i first=1
for status in ${JobStatusValues//,/ }; do
if [[ $first -eq 1 ]]; then
first=0
JobSearchQuery+="Status=$status"
else
JobSearchQuery+="|Status=$status"
fi
done
JobSearchQuery+=")"
fi
else
# Query to show only "Open" J2J jobs. See usage info for this script and
# 'p4 jobspec -o' for details on all states.
JobSearchQuery="Project=$WORKSHOP_PROJECT ^Status=closed ^Status=duplicate ^Status=fixed ^Status=obsolete ^Status=punted ^Status=suspended"
fi
else
ReportStyleTitle="Open Jobs"
# Query to show all J2J jobs.
JobSearchQuery="Project=$WORKSHOP_PROJECT"
fi
[[ "$ShowUserJobsOnly" -eq 1 ]] && ReportStyleTitle+=" owned by $WorkshopUser"
vmsg "Job Search Query: $JobSearchQuery"
printf "J2J Jobs Report showing $ReportStyleTitle\\n\\n %-8s %-12s %-2s %-16s %-16s %-12s %-s\\n" "Job" "Status" "Pr" "Reporter" "Owner" "Title"
printf " %-8s %-12s %-2s %-16s %-16s %-s\\n" "--------" "------------" "--" "----------------" "----------------" "----------------------------------------------------------------------"
if [[ "$GenCSVFile" -eq 1 ]]; then
echo "IssueKey,Status,Priority,ReportedBy,Owner,Title,Description,DevNotes" > "$CSVFile" ||\
bail "Could not generate CSV file: $CSVFile"
fi
TmpFile=$(mktemp)
p4 -ztag jobs -e "$JobSearchQuery" > "$TmpFile" ||\
bail "Could not run jobs query command: p4 -ztag jobs -e \"$JobSearchQuery\""
# This loop processes lines in a text file containing tagged info for many jobs.
# Each loop iteration processes just one line in the output file, but several
# lines contitute a job. The order in which fields are displayed is known: The
# 'Job' field is always first, and the Description field last. When the last
# field is encountered, data for the current job is complete, and output can be
# displayed for that job. Within the sequence of lines for any given job,
# there will be exactly one appearance of required fields like Job or Status.
# Optional fields, like DevNotes, may or may not appear in any given block;
# such fields must be initiailzed to null each time a new job starts.
while read -r Line; do
JobDataComplete=0
DisplayJob=1
# Initialize optional values for fields that may not have any output in the
# 'p4 jobs' command above.
if [[ "$Line" =~ ^\.\.\.\ Job ]]; then
Job=${Line#... Job }
# DevNotes is an optional field in a job. Clear it when we start
# processing a new job.
DevNotes=Unset
FullDescription=Unset
elif [[ "$Line" =~ ^\.\.\.\ Status ]]; then
Status=${Line#... Status }
elif [[ "$Line" =~ ^\.\.\.\ Type ]]; then
Type=${Line#... Type }
elif [[ "$Line" =~ ^\.\.\.\ Severity ]]; then
# We interpret 'Severity' as 'Priority'.
Priority=${Line#... Severity }
elif [[ "$Line" =~ ^\.\.\.\ OwnedBy ]]; then
OwnedBy=${Line#... OwnedBy }
elif [[ "$Line" =~ ^\.\.\.\ ReportedBy ]]; then
ReportedBy=${Line#... ReportedBy }
elif [[ "$Line" =~ ^\.\.\.\ DevNotes ]]; then
DevNotes=${Line#... DevNotes }
elif [[ "$Line" =~ ^\.\.\.\ Description ]]; then
Title=${Line#... Description }
FullDescription="$Title"
Title=$(echo "$Title" | cut -c -"$MaxTitleLength")
JobDataComplete=1
else
# There are 2 multi-line fields, DevNotes and FullDescription.
# Append extra lines to the correct one.
if [[ "$DevNotes" == Unset ]]; then
FullDescription+=" $Line"
else
DevNotes+=" $Line"
fi
fi
# When data for a job is fully loaded, decide if we show it.
if [[ "$JobDataComplete" -eq 1 ]]; then
[[ "$DevNotes" == Unset ]] && DevNotes=
# Skip jobs by users other than the current user of '-me' was specified.
if [[ $ShowUserJobsOnly -eq 1 ]]; then
if [[ "$WorkshopUser" == "none" ]]; then
[[ -n "$OwnedBy" ]] || DisplayJob=0
else
[[ "$OwnedBy" != "$WorkshopUser" ]] || DisplayJob=0
fi
fi
fi
[[ "$DisplayJob" -eq 1 ]] || continue
if [[ "$JobDataComplete" -eq 1 ]]; then
JobDataComplete=0
JobType[$Job]="$Type"
JobStatus[$Job]="$Status"
JobPriority[$Job]="$Priority"
JobOwnedBy[$Job]="$OwnedBy"
JobReportedBy[$Job]="$ReportedBy"
JobFullDescription[$Job]="$FullDescription"
JobDevNotes[$Job]="$DevNotes"
JobTitle[$Job]="$Title"
JobCount+=1
fi
done < "$TmpFile"
for Job in $(echo "${!JobType[@]}" | tr ' ' '\n' | sort -t'-' -k2n | tr '\n' ' '); do
printf " %-8s %-12s %-2s %-16s %-16s %-12s %-s\\n" "$Job" "${JobStatus[$Job]}" "${JobPriority[$Job]}" "${JobReportedBy[$Job]}" "${JobOwnedBy[$Job]}" "${JobTitle[$Job]}"
# If requested, generate a CSV file. Replaces commas in values so as not to break the file.
if [[ "$GenCSVFile" -eq 1 ]]; then
Status="${JobStatus[$Job]}"
Status=${Status/,/COMMA}
Priority="${JobPriority[$Job]}"
Priority=${Priority/,/COMMA}
ReportedBy="${JobReportedBy[$Job]}"
ReportedBy=${ReportedBy/,/COMMA}
OwnedBy="${JobOwnedBy[$Job]}"
OwnedBy=${OwnedBy/,/COMMA}
Title="${JobTitle[$Job]}"
Title=${Title/,/COMMA}
FullDescription="${JobFullDescription[$Job]}"
FullDescription=${FullDescription/,/COMMA}
DevNotes="${JobDevNotes[$Job]}"
DevNotes=${DevNotes/,/COMMA}
echo "$Job,${JobStatus[$Job]},${JobPriority[$Job]},${JobReportedBy[$Job]},${JobOwnedBy[$Job]},${JobTitle[$Job]},${JobFullDescription[$Job]},${JobDevNotes[$Job]}" >> "$CSVFile" ||\
errmsg "Problem writing data to: $CSVFile"
fi
done
if [[ "$Debug" -eq 0 ]]; then
rm -f "$TmpFile"
else
msg "TmpFile: $TmpFile"
fi
[[ -n "$CSVFile" ]] && msg "\\nGenerated CSF File is: $CSVFile"
msg "${H}\\n$JobCount jobs reported in $((SECONDS/3600)) hours $((SECONDS%3600/60)) minutes $((SECONDS%60)) seconds.\n"
# See the terminate() function, which is really where this script exits.
exit 0