zsh-users / zsh-completions

Additional completion definitions for Zsh.
Other
6.91k stars 714 forks source link

Assistance creating completion #709

Closed Jab2870 closed 4 years ago

Jab2870 commented 4 years ago

Hi,

I am quite new to zsh completions but am trying to write one for the hashcat program.

I have the following code that parses the help command and puts it into the argument[description] format required by the _arguments helper function. I have tried parsing it to xargs or putting it in an array but am struggling.

    hashcat --help | sed -n '/Options/,/Hash modes/p' | tail -n +5 | head -n -2 | while read line; do
        op="$(echo "$line" | cut -d"|" -f1 | trim)"
        description="$(echo "$line" | cut -d"|" -f3 | trim)"
        echo "$op" | trim | sed 's/[[:space:]]*$//' | awk "{print \$1 \"[$description]\"}"
    done

Any help would be much appreciated. Apologies if I am asking in the wrong place.

Thanks in advance

syohex commented 4 years ago

I'm not shell script expert and I don't understand your shell script well. I suppose it is difficult to parse hashcat --help output and generate zsh completion file in shell script. I suppose it is better to use other programming language, like python, perl ruby etc, rather than shell script as below.

#!/usr/bin/env python3
import re
import subprocess
import sys

def main():
    proc = subprocess.run(['hashcat', '--help'], stdout=subprocess.PIPE)
    if proc.returncode != 0:
        print('Failed to execute "hashcat --help"')
        sys.exit(1)

    output = proc.stdout.decode('utf-8')

    current_mode = ''

    print('#compdef hashcat')
    print('_arguments \\')
    for line in output.splitlines():
        match = re.match(r'-\s+\[\s+(\S+)', line)
        if match is not None:
            next_mode = match.group(1)
            if current_mode == 'Options':
                break
            current_mode = next_mode
            continue

        if current_mode != 'Options':
            continue

        if re.search(r'^\s+-', line) is None:
            continue

        option, _type, desc, _example = [part.strip() for part in line.split('|')]
        desc = desc.replace("'", "\\'").replace('$', '\\$').replace('[', '\\[').replace(']', '\\]')
        if ',' in option:
            options = ','.join([opt.strip() for opt in option.split(',')])
            print(f'{{{options}}}"[{desc}]" \\')
        else:
            print(f'"{option}[{desc}]" \\')

    print('')

if __name__ == '__main__':
    main()
python3 above_script.py
Output

```zsh #compdef hashcat _arguments \ {-m,--hash-type}"[Hash-type, see references below]" \ {-a,--attack-mode}"[Attack-mode, see references below]" \ {-V,--version}"[Print version]" \ {-h,--help}"[Print help]" \ "--quiet[Suppress output]" \ "--hex-charset[Assume charset is given in hex]" \ "--hex-salt[Assume salt is given in hex]" \ "--hex-wordlist[Assume words in wordlist are given in hex]" \ "--force[Ignore warnings]" \ "--status[Enable automatic update of the status screen]" \ "--status-timer[Sets seconds between status screen updates to X]" \ "--stdin-timeout-abort[Abort if there is no input from stdin for X seconds]" \ "--machine-readable[Display the status view in a machine-readable format]" \ "--keep-guessing[Keep guessing the hash after it has been cracked]" \ "--self-test-disable[Disable self-test functionality on startup]" \ "--loopback[Add new plains to induct directory]" \ "--markov-hcstat2[Specify hcstat2 file to use]" \ "--markov-disable[Disables markov-chains, emulates classic brute-force]" \ "--markov-classic[Enables classic markov-chains, no per-position]" \ {-t,--markov-threshold}"[Threshold X when to stop accepting new markov-chains]" \ "--runtime[Abort session after X seconds of runtime]" \ "--session[Define specific session name]" \ "--restore[Restore session from --session]" \ "--restore-disable[Do not write restore file]" \ "--restore-file-path[Specific path to restore file]" \ {-o,--outfile}"[Define outfile for recovered hash]" \ "--outfile-format[Define outfile-format X for recovered hash]" \ "--outfile-autohex-disable[Disable the use of \$HEX\[\] in output plains]" \ "--outfile-check-timer[Sets seconds between outfile checks to X]" \ "--wordlist-autohex-disable[Disable the conversion of \$HEX\[\] from the wordlist]" \ {-p,--separator}"[Separator char for hashlists and outfile]" \ "--stdout[Do not crack a hash, instead print candidates only]" \ "--show[Compare hashlist with potfile; show cracked hashes]" \ "--left[Compare hashlist with potfile; show uncracked hashes]" \ "--username[Enable ignoring of usernames in hashfile]" \ "--remove[Enable removal of hashes once they are cracked]" \ "--remove-timer[Update input hash file each X seconds]" \ "--potfile-disable[Do not write potfile]" \ "--potfile-path[Specific path to potfile]" \ "--encoding-from[Force internal wordlist encoding from X]" \ "--encoding-to[Force internal wordlist encoding to X]" \ "--debug-mode[Defines the debug mode (hybrid only by using rules)]" \ "--debug-file[Output file for debugging rules]" \ "--induction-dir[Specify the induction directory to use for loopback]" \ "--outfile-check-dir[Specify the outfile directory to monitor for plains]" \ "--logfile-disable[Disable the logfile]" \ "--hccapx-message-pair[Load only message pairs from hccapx matching X]" \ "--nonce-error-corrections[The BF size range to replace AP\'s nonce last bytes]" \ "--keyboard-layout-mapping[Keyboard layout mapping table for special hash-modes]" \ "--truecrypt-keyfiles[Keyfiles to use, separated with commas]" \ "--veracrypt-keyfiles[Keyfiles to use, separated with commas]" \ "--veracrypt-pim[VeraCrypt personal iterations multiplier]" \ {-b,--benchmark}"[Run benchmark of selected hash-modes]" \ "--benchmark-all[Run benchmark of all hash-modes (requires -b)]" \ "--speed-only[Return expected speed of the attack, then quit]" \ "--progress-only[Return ideal progress step size and time to process]" \ {-c,--segment-size}"[Sets size in MB to cache from the wordfile to X]" \ "--bitmap-min[Sets minimum bits allowed for bitmaps to X]" \ "--bitmap-max[Sets maximum bits allowed for bitmaps to X]" \ "--cpu-affinity[Locks to CPU devices, separated with commas]" \ "--example-hashes[Show an example hash for each hash-mode]" \ {-I,--opencl-info}"[Show info about detected OpenCL platforms/devices]" \ "--opencl-platforms[OpenCL platforms to use, separated with commas]" \ {-d,--opencl-devices}"[OpenCL devices to use, separated with commas]" \ {-D,--opencl-device-types}"[OpenCL device-types to use, separated with commas]" \ "--opencl-vector-width[Manually override OpenCL vector-width to X]" \ {-O,--optimized-kernel-enable}"[Enable optimized kernels (limits password length)]" \ {-w,--workload-profile}"[Enable a specific workload profile, see pool below]" \ {-n,--kernel-accel}"[Manual workload tuning, set outerloop step size to X]" \ {-u,--kernel-loops}"[Manual workload tuning, set innerloop step size to X]" \ {-T,--kernel-threads}"[Manual workload tuning, set thread count to X]" \ "--spin-damp[Use CPU for device synchronization, in percent]" \ "--hwmon-disable[Disable temperature and fanspeed reads and triggers]" \ "--hwmon-temp-abort[Abort if temperature reaches X degrees Celsius]" \ "--scrypt-tmto[Manually override TMTO value for scrypt to X]" \ {-s,--skip}"[Skip X words from the start]" \ {-l,--limit}"[Limit X words from the start + skipped words]" \ "--keyspace[Show keyspace base:mod values and quit]" \ {-j,--rule-left}"[Single rule applied to each word from left wordlist]" \ {-k,--rule-right}"[Single rule applied to each word from right wordlist]" \ {-r,--rules-file}"[Multiple rules applied to each word from wordlists]" \ {-g,--generate-rules}"[Generate X random rules]" \ "--generate-rules-func-min[Force min X functions per rule]" \ "--generate-rules-func-max[Force max X functions per rule]" \ "--generate-rules-seed[Force RNG seed set to X]" \ {-1,--custom-charset1}"[User-defined charset ?1]" \ {-2,--custom-charset2}"[User-defined charset ?2]" \ {-3,--custom-charset3}"[User-defined charset ?3]" \ {-4,--custom-charset4}"[User-defined charset ?4]" \ {-i,--increment}"[Enable mask increment mode]" \ "--increment-min[Start mask incrementing at X]" \ "--increment-max[Stop mask incrementing at X]" \ {-S,--slow-candidates}"[Enable slower (but advanced) candidate generators]" \ "--brain-server[Enable brain server]" \ {-z,--brain-client}"[Enable brain client, activates -S]" \ "--brain-client-features[Define brain client features, see below]" \ "--brain-host[Brain server host (IP or domain)]" \ "--brain-port[Brain server port]" \ "--brain-password[Brain server authentication password]" \ "--brain-session[Overrides automatically calculated brain session]" \ "--brain-session-whitelist[Allow given sessions only, separated with commas]" \ ```

Jab2870 commented 4 years ago

Hi @syohex , thanks for your help. I wasn't trying to use the shell script to generate a completion file, I was trying to use it IN the completion file. My thought being, if / when new options are added, I won't need to re-generate my completion script.

Here is my full completion script _hashcat:

#compdef hashcat
#
trim(){
    cat - | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//'
}

_args(){
    hashcat --help | sed -n '/Options/,/Hash modes/p' | tail -n +5 | head -n -2 | while read line; do
        op="$(echo "$line" | cut -d"|" -f1 | trim)"
        description="$(echo "$line" | cut -d"|" -f3 | trim)"
        echo "$op" | sed 's/,/\n/' | trim | awk "{print \$1 \":$description\"}"
    done
}

args=("${(f)$(_args)}")

_describe "Help" args

Using _describe, this works. I get a list of options for hashcat. Here is a snippet

--restore-disable                                 -- Do not write restore file
--restore-file-path                               -- Specific path to restore file
--rule-left                 -j                    -- Single rule applied to each word from left wordlist
--rule-right                -k                    -- Single rule applied to each word from right wordlist
--rules-file                -r                    -- Multiple rules applied to each word from wordlists
--runtime                                         -- Abort session after X seconds of runtime
--scrypt-tmto                                     -- Manually override TMTO value for scrypt to X
--segment-size              -c                    -- Sets size in MB to cache from the wordfile to X
--self-test-disable                               -- Disable self-test functionality on startup

However, if my understanding is correct, I can't use describe if I want to use a different completion function for positional arguments. For example, after --hash-type, I would like a different function that will parse a different part of the help text.

syohex commented 4 years ago

if my understanding is correct

I suppose your understanding is correct

You need to use _arguments or similar functions and generate suitable specs for each options instead of using _describe.

Jab2870 commented 4 years ago

Thanks,

That would still lead to having to re-generate the completion each time the command changes. Is there a reason I shouldn't do something like this?

#compdef hashcat
#
tokens=(${(z)LBUFFER})

if [[ "${LBUFFER[-1]}" == " " ]]; then
    previousArg="${tokens[-1]}"
else
    previousArg="${tokens[-2]}"
fi

trim(){
    cat - | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//'
}

_args(){
    hashcat --help | sed -n '/Options/,/Hash modes/p' | tail -n +5 | head -n -2 | while read line; do
        op="$(echo "$line" | cut -d"|" -f1 | trim)"
        description="$(echo "$line" | cut -d"|" -f3 | trim)"
        echo "$op" | sed 's/,/\n/' | trim | awk "{print \$1 \":$description\"}"
    done
}
args=("${(f)$(_args)}")

_hashes(){
    hashcat --example-hashes | awk -v RS="\n\n" -F "\t" '{gsub("\n","\t",$0); print $1 ":" $2 }' | sed 's/MODE: //; s/TYPE: //'
}
hashes=("${(f)$(_hashes)}")

case "$previousArg" in
    -m|--hash-type) _describe "Hashes" hashes ;;
    *) _describe "Help" args ;;
esac
syohex commented 4 years ago

Is there a reason I shouldn't do something like this?

No. It is good if you have no problems