Here's on way you can do it with DFSORT.
First, this step:
//PUNPDS EXEC PGM=IEBPTPCH
//SYSPRINT DD SYSOUT=*
//SYSIN DD *
PUNCH TYPORG=PO,MAXNAME=1,MAXFLDS=1
RECORD FIELD=(80)
//SYSUT1 DD DISP=OLD,DSN=your pds
//SYSUT2 DD DSN=&&PDSUNL,
// DISP=(,PASS),UNIT=SYSDA,SPACE=(CYL,(1,1))
//*
You may need to adjust the SPACE parameter, I tested on a PDS with not many more than 100 members, and not very big ones.
That uses the IBM Utility IEBPTPCH, which "prints" or "punches" a dataset, which can be a PDS/PDSE (TYPORG=PO).
The output will be 81-byte records with a leading "control character", which we'll ignore. Each member is prefixed by a piece of text naming the member.
The output will include all members, it is a PDS/PDSE that has been "flattened" to a sequential dataset.
Then process that dataset with SORT.
//FINDDUPS EXEC PGM=SORT
//SYSOUT DD SYSOUT=*
//SORTOUT DD SYSOUT=*
//SORTIN DD DSN=&&PDSUNL,DISP=(OLD,PASS)
//SYMNAMES DD *
RECORD-FIRST-TWO,2,2,CH
RECORD-FIRST-THREE,=,3,CH
PUNCH-CHAR,1,1,CH
WHOLE-RECORD,*,80,CH
RECORD-EXTENSION,*
EXT-MEMBER-NAME,*,8,CH
EXT-SEQUENCE,*,5,CH
POSITION,WHOLE-RECORD
DATA-IN-RECORD,=,72
MEMBER-POSITION,=,13,CH
MEMBER-NAME,*,8,CH
OUT-MEMBER-NAME,1,8,CH
OUT-STEP-NAME,*,8,CH
OUT-SEQUENCE,*,5,CH
OUT-WHOLE-RECORD,*,80,CH
OUT-SHOULD-BE-EXEC,*,4,CH
OUT-EXT,*
OUT-STEP-SEQUENCE,*,3,CH
POSITION,OUT-MEMBER-NAME
OUT-SORT-KEY,=,16,CH
STEP-NAME,%00
SHOULD-BE-EXEC,%01
JCL-START,C'//'
EXEC-FOR-TEST,C'EXEC'
EXEC-STATEMENT,C' EXEC '
JCL-COMMENT-OR-JES-COMMAND,C'//*'
START-OF-MEMBER,C'MEMBER NAME '
ZERO-TO-IGNORE,C'00000'
MEMBER-NAME-CARD,C'00001'
FIRST-STEP-IN-JOB,C'001'
A-BLANK,C' '
ALL-8-BLANK,C' '
//SYMNOUT DD SYSOUT=*
//SYSIN DD *
INCLUDE COND=(RECORD-FIRST-TWO,
EQ,
JCL-START,
AND,
(RECORD-FIRST-THREE,
NE,
JCL-COMMENT-OR-JES-COMMAND,
AND,
(DATA-IN-RECORD,SS,
EQ,
EXEC-STATEMENT)),
OR,
(MEMBER-POSITION,
EQ,
START-OF-MEMBER))
INREC IFTHEN=(WHEN=GROUP,
BEGIN=(MEMBER-POSITION,
EQ,
START-OF-MEMBER),
PUSH=(RECORD-EXTENSION:
MEMBER-NAME,
SEQ=5)),
IFTHEN=(WHEN=INIT,
PARSE=(STEP-NAME=(STARTAFT=JCL-START,
ENDBEFR=A-BLANK,FIXLEN=8),
SHOULD-BE-EXEC=(SUBPOS=1,
STARTAFT=BLANKS,
ENDBEFR=A-BLANK,
FIXLEN=4))),
IFTHEN=(WHEN=INIT,
BUILD=(EXT-MEMBER-NAME,
STEP-NAME,
EXT-SEQUENCE,
WHOLE-RECORD,
SHOULD-BE-EXEC)),
IFTHEN=(WHEN=(OUT-SEQUENCE,
EQ,
MEMBER-NAME-CARD),
OVERLAY=(OUT-MEMBER-NAME:
ALL-8-BLANK)),
IFTHEN=(WHEN=(OUT-SHOULD-BE-EXEC,
NE,
EXEC-FOR-TEST),
OVERLAY=(OUT-SEQUENCE:
ZERO-TO-IGNORE))
SORT FIELDS=(OUT-SORT-KEY,A)
OUTREC IFTHEN=(WHEN=GROUP,
KEYBEGIN=(OUT-SORT-KEY),
PUSH=(OUT-STEP-SEQUENCE:
SEQ=3))
OUTFIL INCLUDE=(OUT-SEQUENCE,
GT,
MEMBER-NAME-CARD,
AND,
OUT-STEP-SEQUENCE,
GT,
FIRST-STEP-IN-JOB),
HEADER1=('DUPLICATE STEPS PER MEMBER')
I've used SORT symbols (defined on the SYMNAMES dataset, and listed on the SYMNOUT dataset) to make this easier to follow.
Firstly, INCLUDE COND= is used to select only the data which may be of use to you. This is the generated member-name card, and any JCL cards (start with // and not with //*) that may contain EXEC, bounded by space to avoid false hits.
This will give you cards like this
MEMBER NAME XXXXXXX
// EXEC EXEC PGM=X
//STEP EXEC PGM=X
//INPUT DD DSN=ABC.DEF.GHI, THE OUTPUT FROM THE PREVIOUS EXEC
The inline comments on JCL conspire against you, but we'll deal with those.
Define a GROUP on identifying the MEMBER NAME. Save the member-name itself to an extension to the current record, along with a sequence number within the group.
Use PARSE to get the stepname, and to get the second "word" on the record. Note the SUBPOS=1. This is necessary because a single blank ending the step-name can also be the only blank available to delimit the second word on the line, so the parse-pointer has to be set back one.
BUILD a new record, with the member-name, step-name, sequence number, whole line, and the second word on the line.
Then there are two cases we want to "clobber" so they don't get selected later. The member-name record is no longer needed, and any lines which happen to have EXEC (from the INCLUDE COND) but where it is not the second word on the line (from the PARSE).
SORT the records.
Use OUTFIL to select what will only be the records with steps, and only those steps whose names are duplicate (all the duplicates will be listed, but not the original - one output line with a step will indicate two source lines, two output lines for the same step would indicate three source lines). List out the data with a simple heading.
If you are keener on old-style SORT Control Cards, here they are after the conversion of the symbols:
INCLUDE COND=(2,2,CH,EQ,C'//',AND,(2,3,CH,NE,C'//*',AND,(2,72,SS,EQ,C'*
EXEC ')),OR,(2,13,CH,EQ,C'MEMBER NAME '))
INREC IFTHEN=(WHEN=GROUP,BEGIN=(2,13,CH,EQ,C'MEMBER NAME '),PUSH=(82:*
15,8,SEQ=5)),IFTHEN=(WHEN=INIT,PARSE=(%00=(STARTAFT=C'//*
',ENDBEFR=C' ',FIXLEN=8),%01=(SUBPOS=1,STARTAFT=BLANKS,E*
NDBEFR=C' ',FIXLEN=4))),IFTHEN=(WHEN=INIT,BUILD=(82,8,%0*
0,90,5,2,80,%01)),IFTHEN=(WHEN=(17,5,CH,EQ,C'00001'),OVE*
RLAY=(1:C' ')),IFTHEN=(WHEN=(102,4,CH,NE,C'EXEC'),*
OVERLAY=(17:C'00000'))
SORT FIELDS=(1,16,CH,A)
OUTREC IFTHEN=(WHEN=GROUP,KEYBEGIN=(1,16),PUSH=(106:SEQ=3))
OUTFIL INCLUDE=(17,5,CH,GT,C'00001',AND,106,3,CH,GT,C'001'),HEADER1=('*
DUPLICATE STEPS PER MEMBER')