Openwall GNU/*/Linux - a small security-enhanced Linux distro for servers
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Date: Sat, 12 May 2012 22:37:56 +0200
From: Frank Dittrich <frank_dittrich@...mail.com>
To: john-dev@...ts.openwall.com
Subject: Re: Fwd: bash auto-completion for john

On 05/12/2012 01:45 AM, magnum wrote:
> On 05/11/2012 09:44 PM, Frank Dittrich wrote:
>> For --rules= and --single=, I'll do the same.
>> Non-jumbo versions don't allow optional parameters for -rules and
>> single, but there might exist "older" jumbo versions which allow
>> parameters for --rules and --single, but don't have --list=...
> 
> IMHO, I'd say we should (care to) support latest core and latest Jumbo
> (in this case). Sure, the better we support old Jumbos, the better. But
> let's not put hours of work for seconds of benefit.

Trouble is, the completion rules will work for any executable called john:
john
./john
/usr/bin/john

Once a user installs the latest jumbo and copies the bash completion
file to /etc/bash_completion.d/, the same completion logic will also
work for a system-wide john which has been installed as a package of the
linux distribution.
So I don't know whether we should care about this.

I continued working on the script and noticed some more problems
which I "documented" using some FIXME comments:

# FIXME: If there is no .rec file in the current directory, completion
#	 for --restore= and --status= will show all files!
######## echo _`for f in *.rec; do echo ${f%.rec};done`_

# FIXME: for some reason completion for --option= only works
#	 if the cursor is at the end of the command,
#	 i.e. [[ ${COMP_POINT} -eq ${#COMP_LINE} ]]
#	 not if some words follow the one to be completed...
#	 If ${cur#*=} is not empty, completion works even in the middle
#	 of the command line
#	 This is annoying if I want to complete --rules= in
#        ./john --rules= --config=test.conf

In these cases, expansion with correct List.Rules section names from the
config file specified works correctly.

./john -co=test.conf --rules=
./john --rules=w --conf:john.conf

If I specify more than one config file, the last one is used for rules
completion:

./john --config=john.conf --config=fd.conf --rules=

will list the rules sections from fd.conf.


Further changes:

I realized that on my system, the function have() is just a dummy always
returning "yes".
That's why I removed the references to have().

Getting rid of have() usage also has another advantage:
Now it is not even necessary to put the file john.bash_completion into
the /etc/bash_completion.d/ directory.

Adding the line
. <path_to_john_source_directory>/john.bash_completion
to ~/.-bashrc is all that's needed to make bash completion for john and
unique work.

So you could make bash completion work for the current user without
requiring admin privilege, by just sourcing john.bash_completion and
adding a line to ~/bashrc which sources the john.bash_completion file.

Alternatively: If the make bash-completion fails due to insufficient
rights, just display instructions how to make bash completion work for
the current user.

Should we discuss possible changes in the completion logic here, or on
john-users?

E.g., currently I use this expansion logic:

$ ./john -in
Pressing [tab] changes -in to --incremental.

For --incremental, there are two possible completion implementations.

The first one:

$ ./john --incremental
Pressing [tab] changes --incremental to --incremental=

$ ./john --incremental=
Pressing [tab] twice lists these options:

$ ./john --incremental=
all      all15    all6     all7     all8     alnum    alpha    digits
digits8  lanman

This means, the only indication that --incremental is a valid option
even without adding =value is that the first completion step for -in
results in --incremental, not --incremental=.

(For -ex on the other hand, pressing [tab] immediately will result in
--external=, because --external requires a value.)


The other implementation would provide these options for a non-jumbo
version):

$ john --incremental
--incremental         --incremental=Alpha
--incremental=All     --incremental=Digits
--incremental=Alnum   --incremental=LanMan

and these for the current git version:

$ ./john --incremental
--incremental          --incremental=alnum
--incremental=all      --incremental=alpha
--incremental=all15    --incremental=digits
--incremental=all6     --incremental=digits8
--incremental=all7     --incremental=lanman
--incremental=all8

(Of course, the number of columns depends on the width of the terminal.
$ ./john --incremental
--incremental          --incremental=all7     --incremental=digits
--incremental=all      --incremental=all8     --incremental=digits8
--incremental=all15    --incremental=alnum    --incremental=lanman
--incremental=all6     --incremental=alpha


With this output, it would be obvious that --incremental is a valid option.

This behavior would also be consistent with current completion for
--markov (jumbo only), --stdout, and --test

$ ./john --markov
--markov
--markov=LEVEL[:START[:END[:LENGTH]]]

$ john --stdout
--stdout         --stdout=LENGTH

$ john --test
--test          --test=SECONDS

$ ./john --show
--show       --show=LEFT

(But there is no completion logic for ./john --markov= and no completion
for ./john --stdout= and ./john --test= and ./john --show=)


For --incremental, the user would have to type the '=' manually, to get

$ ./john --incremental=
all      all6     all8     alpha    digits8
all15    all7     alnum    digits   lanman

after pressing [tab] twice.

Which logic would be preferable?
Is consistent behavior for all options that can be used with and without
a value more important? Or is a more compact list without the --option=
preferable?
Should we discuss this on john-users?

For current non-jumbo builds these options do have optional values:
--incremental
--stdout
--restore
--status
--test

For current git (next jumbo), these options have to be considered in
addition to the official non-jumbo version:
--single
--rules
--markov
--show

Because I am not sure what to prefer, I'll keep the inconsistent
behavior for now in the attached file:

$ ./john --incremental
--incremental          --incremental=all7     --incremental=digits
--incremental=all      --incremental=all8     --incremental=digits8
--incremental=all15    --incremental=alnum    --incremental=lanman
--incremental=all6     --incremental=alpha

or
$ john --incremental
--incremental         --incremental=Alnum   --incremental=Digits
--incremental=All     --incremental=Alpha   --incremental=LanMan

versus

$ ./john --rules

completing to --rules= after pressing [tab]


We should probably decide which behavior is better and unify it for the
 expansion of --rules (jumbo only), --single (jumbo only),
--incremental, --restore and --status (session names derived from .rec
files) .
(Of course, I could also allow a user specific configuration of the
behavior. But may be this is overkill.)

Frank

# bash completion for john and unique commands (John the Ripper)
#
# This software is Copyright © 2012 Frank Dittrich
# and hereby released to the general public under the following terms:
# Redistribution and use in source and binary forms, with or without 
# modification, are permitted.
#
# Minor improvements suggested by Aleksey Cherepanov have been
# incorporated.
#
#
# This file needs either to be copied into the /etc/bash_completion.d/.
# To make the new completion rules work, you need to logout and login,
# or source /etc/bash_completion instead.
#
# Alternatively, just add a line
# . <path_to_john's_source_directory>/john.bash_completion
# to your ~/.bashrc and logout/login.
#
# To use the same completion rules not just for john, but for 
# differently named binaries (say john-omp, john-sse2i, john-avx,
# john-cuda, john-gpu, ...),
# just use this command to get the current completion rule settings:
#       complete -p john
#
# If the output is
#       complete -F _john john
# you can use this command to activate the same completion rules
# for john-omp:
#       complete -F _john john-omp
#
# To use these completion rules permanently, you might add
#       complete -F _john john-omp
# to your ~/.bashrc file.
#
#
# The code is still ugly, many things can probably be done more efficiently.
# Currently, grep and sed are the only external commands used.
#
#
# Trying to build a perfect completion script will be hard.
# If possible, I'd like to avoid the need to maintain this script whenever
# john gets a new option.
#
# john can either be globally installed (e.g. in /usr/bin/john),
# or it can be installed locally somewhere in a user's home directory.
# It can be an official version, or a community encanced (jumbo) version,
# with a variety of patches.
#
# FIXME: is using __expand_tilde_by_ref OK?
#
# FIXME: If there is no .rec file in the current directory, completion
#	 for --restore= and --status= will show all files!
######## echo _`for f in *.rec; do echo ${f%.rec};done`_
#
# FIXME: for some reason completion for --option= only works 
#	 if the cursor is at the end of the command, 
#	 i.e. [[ ${COMP_POINT} -eq ${#COMP_LINE} ]]
#	 not if some words follow the one to be completed...
#	 If ${cur#*=} is not empty, completion works even in the middle
#	 of the command line
#	 This is annoying if I want to complete --rules= in
#        ./john --rules= --config=test.conf
#
# TODO:
#       --wordlist=~user/filename or --wordlist=~/dir/file doesn't work,
#         but pressing [tab] expands this to something useful
#         Where to fix this? In john? Some bash config option?
#
#       --external should use names of [List.External:..-.] sections
#         (Just sections without a generate() function, if --wordlist, --incremental or --single
#         is present on the command line; with a generate() function, if none of these
#         options is used - WHAT IF the user adds a --wordlist option later?)
#       --rules=, --single=, --incremental=, --external= need to take into account a --config file
#         which might have been specified on the command line, correct default john.conf locations 
#         (/etc/, ~/.john/, or ./, depending on build options), and included config files...
#         FIXME: The user might have used -co: instead of --config..., the user might specify 
#         --config=... later (should we really scan config files for sections referenced by other
#         command line options, to skip config files which do not have the right sections?)
#         Currently -opt is expanded to --option or --option=...
#         FIXME: To be able to locate the correct .conf and .rec files, we might even need a new
#         john option to know the values of JOHN_SYSTEMWIDE ... (e.g. --list=build-options)
#       --restore= and --status= completion should search for existing .rec files 
#	  in the correct directory, instead of $PWD.
#       --mkpc and other options which are not mentioned in the usage output are currently ignored
#
#       Should I support -option instead of --option? (currently -option (or -opt) gets replaced 
#	with --option during completion.
#	Should I support --option:val, -option:val, or even -opt:val instead of just --option=val?
#	Should expanding an abbreviated option to its long form also be done by john itself
#	(requires new john option)?


# grep and sed are used to process john's usage info the list of .rec files...

# john
## on my system, have() is a dummy function which always return "yes", so get rid of calling it... 
## have grep && have sed && 
_john()
{
	local first cur options valopts compreplya compreplyb encodings formats subformats list hidden dir cmd
	COMPREPLY=()
	_get_comp_words_by_ref -n = cur

#	we need to make sure we run the correct program, not some other program 
#	called john which is located somewhere in $PATH
	first="${COMP_WORDS[0]}"
#	Most options are listed at the begin of the line, but the line with the --pipe option
#	does have trailing spaces, and --stdin is mentioned after --wordlist=FILE.
#
#	all options (the '=' will be emoved for options with an optional value)
	options=""
# FIXME: How do I suppress the error message if someone tries to be clever: cd run; ./john --[tab] ???
	options="`${first}|sed -n '{ s#^ *\(--[a-z-]*=\?\(LIST\)\?\).*$#\1# }; /^--/ p'` --stdin"
	if [[ "_${options}" == "_" ]] ; then
		compopt -F _filedir_xspec
		return 0
	fi

#	Just those options that can be used together with a value, even if that value is optional:
	valopts=`${first}|grep '^ *--[a-z\[-]*='|grep -v '^ *--subformat='|sed 's#^ *\([a-z=-]*\).*$#\1#'`
#	This is used to decide whether or not the completion should add a trailing space.
#	(That means, for a jumbo build, --rules doesn't get a trailing space, but for the john version
#	distributed by fedora16, --rules does get a trailing space during completion.
#	The same applies for --show and single)

#	now add the "hidden options" (not mentioned in the usage output, but in doc/OPTIONS and 
#       with --list=hidden-options
#	Currently, all hidden options do have mandatory values (--option=value), this makes
#       addition of these easier
	hidden=""
	hidden=`${first} --list=hidden-options 2>/dev/null|sed 's#^\(--[a-z-]*=\?\).*$#\1#'`

	case "${cur}" in
		--format=dynamic*)
			if echo "${options}" | grep "^--subformat" > /dev/null ; then
				 subformats=`${first} --subformat=LIST|sed 's#^\(User\)\?Format = \(dynamic_[0-9]*\).*$#\2#'`
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "${subformats}" -- ${cur}) )
			fi
			return 0
			;;
		--format=dy|--format=dyn|--format=dyna|--format=dynam|--format=dynami)
			if echo "${options}" | grep "^--subformat" > /dev/null ; then
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "dynamic" -- ${cur}) )
				compopt -o nospace
			fi
			return 0
			;;
		--format=*)
			cur=${cur#*=}
			formats=`${first} |sed -n '/^--format/,$ { s#^--format=[ A-Za-z]*:##; /^--/ q; s#^ *##; s#\<dynamic_n\>#dynamic#; s#[/ ]#\n#g; p }'`
			COMPREPLY=( $(compgen -W "${formats}" -- ${cur}) )
			return 0
			;;

		--restore|--status)
			COMPREPLY=( $(compgen -W "${cur}=" -- ${cur}) )
			compopt -o nospace
			return 0
			;;
		--restore=*|--status=*)
# FIXME: If there is no .rec file in the current directory, completion will show all files!
#	 Where did I see this logic? There must be the same error!
######## echo _`for f in *.rec; do echo ${f%.rec};done`_
			cur=${cur#*=}
## This isn't that easy:
#			dir=`${first} --list=build-info 2>/dev/null|sed -n '/^\$JOHN is / s#^\$JOHN is ##; p'`
#			if [[ "_${dir}" == "_" ]] ; then
			COMPREPLY=( $(compgen -W "$(for f in *.rec; do echo ${f%.rec};done)" -- ${cur}) )
#			else
#			COMPREPLY=( $(compgen -W "$( ( cd ${dir}; for f in *.rec; do echo ${f%.rec};done)" -- ${cur}) )
#			fi
			return 0
			;;
		--wordlist=*)
			#cur=${cur#*=}
			#_filedir expansion of --wordlist=~/te doesn't work
			# _filedir_xspec "_xspecs: bad array subscript" written to stderr
			_filedir_xspec 2> /dev/null
			return 0
			;;
 		--rules|--single)
			if echo "${valopts}" | grep "^${cur}$" > /dev/null ; then
				COMPREPLY=( $(compgen -W "${cur}=" -- ${cur}) )
				compopt -o nospace
			else
				COMPREPLY=( $(compgen -W "${cur}" -- ${cur}) )
			fi
			return 0
			;;
		--rules=*|--single=*)
			if echo "${valopts}" | grep "^${cur%=*}$" > /dev/null ; then
				cmd=`echo ${COMP_LINE}|sed "s# ${cur}# --list=rules #"`
				list=`${cmd} 2>&1`
				if [[ $? -ne 0 ]] ; then
					list=`${first} --list=rules 2>&1`
				fi
				if [[ $? -eq 0 ]] ; then
					cur=${cur#*=}
					COMPREPLY=( $(compgen -W "${list}" -- ${cur}) )
				else
					cur=${cur#*=}
					COMPREPLY=( $(compgen -W "NT single wordlist" -- ${cur}) )
				fi
			fi
			return 0
			;;
		--external=*)
			cmd=`echo ${COMP_LINE}|sed "s# ${cur}# --list=externals #"`
			list=`${cmd} 2>&1`
			if [[ $? -ne 0 ]] ; then
				list=`${first} --list=externals 2>&1`
			fi
			if [[ $? -eq 0 ]] ; then
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "${list}" -- ${cur}) )
			else
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "Filter_Alpha Filter_Digits Filter_Alnum Filter_LanMan LanMan Double Parallel Strip Keyboard DumbForce KnownForce DateTime Repeats Subsets AtLeast1-Simple AtLeast1-Generic Policy AppendLuhn" -- ${cur}) )
			fi
			return 0
			;;
		--incremental)
			cmd=`echo ${COMP_LINE}|sed "s# ${cur}# --list=inc-modes #"`
			list=`${cmd} 2>&1`
			list=`${cmd} 2>&1`
			if [[ $? -ne 0 ]] ; then
				list=`${first} --list=inc-modes 2>&1`
			fi
			if [[ $? -ne 0 ]] ; then
				list="All Alpha Digits Alnum LanMan"
			fi
			list=`echo "${list}"|sed 's# #\n#g'|sed 's#^\(.\)#--incremental=\1#'`
			list="${list} --incremental"
#echo "list4_${list}_"
			COMPREPLY=( $(compgen -W "${list}" -- ${cur}) )
			return 0
			;;
		--incremental=*)
			cmd=`echo ${COMP_LINE}|sed "s# ${cur}# --list=inc-modes #"`
			list=`${cmd} 2>&1`
			if [[ $? -ne 0 ]] ; then
				list=`${first} --list=inc-modes 2>&1`
			fi
			if [[ $? -eq 0 ]] ; then
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "${list}" -- ${cur}) )
			else
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "All Alpha Digits Alnum LanMan" -- ${cur}) )
			fi
			return 0
			;;
		--make-charset=*)
			cur=${cur#*=}
			#redirect stderr just in case __expand_tilde_by_ref
			#doesn't exist everywhere
			#(I'm a bit worried because of the __ at the begin.
			#May be this function isn't part of an "official" API.)
			__expand_tilde_by_ref cur 2>/dev/null
# FIXME:		should I just use directories for completion, not files, 
# FIXME:		to make overwriting existing files harder?
			_filedir "chr"
			return 0
			;;
		--stdout=*|--markov=*)
			return 0
			;;
		--stdout)
			COMPREPLY=( $(compgen -W "--stdout --stdout=LENGTH" -- ${cur}) )
			return 0
			;;
		--markov)
			if echo "${options}" | grep "^${cur}$" > /dev/null ; then
				COMPREPLY=( $(compgen -W "--markov --markov=LEVEL[:START[:END[:LENGTH]]]" -- ${cur}) )
			fi
			return 0
			;;
		--test)
			if echo "${valopts}" | grep "^${cur}$" > /dev/null ; then
				COMPREPLY=( $(compgen -W "--test --test=SECONDS" -- ${cur}) )
			else
				COMPREPLY=( $(compgen -W "${cur}" -- ${cur}) )
			fi
			return 0
			;;
		--show)
			if echo "${valopts}" | grep "^--show$" > /dev/null ; then
				COMPREPLY=( $(compgen -W "--show --show=LEFT" -- ${cur}) )
			else
				COMPREPLY=( $(compgen -W "--show" -- ${cur}) )
			fi
			return 0
			;;
		--show=l*)
			if echo "${valopts}" | grep "^--show$" > /dev/null ; then
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "left" -- ${cur}) )
			fi
			return 0
			;;
		--show=*)
			if echo "${valopts}" | grep "^--show$" > /dev/null ; then
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "LEFT" -- ${cur}) )
			fi
			return 0
			;;
		--users=L*|--users=U*|--users=-*|--groups=G*|--groups=-*|--shells=S*|--shells=-*|--salts=C*|--salts=-*)
			return 0
			;;
		--users=*)
			cur=${cur#*=}
			COMPREPLY=( $(compgen -W "LOGIN,... UID,... -LOGIN,... -UID,..." -- ${cur}) )
			return 0
			;;
		--groups=*)
			cur=${cur#*=}
			COMPREPLY=( $(compgen -W "GID,... -GID,..." -- ${cur}) )
			return 0
			;;
		--shells=*)
			cur=${cur#*=}
			COMPREPLY=( $(compgen -W "SHELL,... -SHELL,..." -- ${cur}) )
			return 0
			;;
		--salts=*)
			cur=${cur#*=}
			COMPREPLY=( $(compgen -W "COUNT -COUNT" -- ${cur}) )
			return 0
			;;
		--encoding=*)
			if  echo "${options}" | grep "^--encoding=" > /dev/null ; then
				encodings=`${first} --encoding=LIST 2>&1|grep -v 'Supported encodings'|sed 's#[,)]##g'|sed 's#(or ##g'`
			cur=${cur#*=}
				if [[ ${COMP_CWORD} -eq 2 || ${COMP_CWORD} -eq 3 && "_${cur}" != "_" ]] ; then
					encodings="${encodings} LIST"
					# make sure LIST will be the first option:
					LC_ALL=C
				fi
				COMPREPLY=( $(compgen -W "${encodings}" -- ${cur}) )
			fi
			return 0
			;;
		--pot=*)
			cur=${cur#*=}
			#redirect stderr just in case __expand_tilde_by_ref
			#doesn't exist everywhere
			#(I'm a bit worried because of the __ at the begin.
			#May be this function isn't part of an "official" API.)
			#
			__expand_tilde_by_ref cur 2>/dev/null
			_filedir "pot"
			return 0
			;;
		--config=*)
			cur=${cur#*=}
			__expand_tilde_by_ref cur 2>/dev/null
			_filedir '@...nf|ini)'
			return 0
			;;
		--save-memory=*)
			cur=${cur#*=}
			COMPREPLY=( $(compgen -W "1 2 3" -- ${cur}) )
			return 0
			;;
		--regen-lost-salts=*)
			if echo "${options}" | grep "^--regen-lost-salts=" > /dev/null ; then
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "1 2 3 4 5" -- ${cur}) )
			fi
			return 0
			;;
		--subformat=l*)
			if echo "${options}" | grep "^--subformat=" > /dev/null ; then
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "list" -- ${cur}) )
			fi
			return 0
			;;
		--subformat=*)
			if echo "${options}" | grep "^--subformat=" > /dev/null ; then
				cur=${cur#*=}
				COMPREPLY=( $(compgen -W "LIST" -- ${cur}) )
			fi
			return 0
			;;
		--mem-file-size=*|--field-separator-char=*|--fix-state-delay=*|--max-run-time=*)
			return 0
			;;
		--list=*)
			if echo "${hidden}" | grep "^--list=" > /dev/null ; then
				cur=${cur#*=}
				list=`${first} --list=? 2>/dev/null|sed 's# or .*$# options#; s#,##g'`
				if [[ $? -eq 0 ]] ; then 
					COMPREPLY=( $(compgen -W "${list} ?" -- ${cur}) )
				fi
			fi
			return 0
			;;
		--mkpc=*)
			return 0
			;;
		-*)
			compreplya=`compgen -W "${options} ${hidden}" -- ${cur}`
			if [[ "_${compreplya}_" == "__" ]] ; then
				cur="-${cur}"
				compreplya=`compgen -W "${options} ${hidden}" -- ${cur}`
			fi
			compreplyb=`compgen -W "${valopts} ${hidden}" -- ${cur}`
			COMPREPLY=( $(compgen -W "${options} ${hidden}" -- ${cur}) )
			if [[ "_${compreplya}" == "_${compreplyb}" ]] ; then
				compopt -o nospace
			fi
			return 0
			;;
		*)
			_filedir
			return 0
			;;
	esac
} &&
complete -F _john john

# unique
## have grep && have sed &&
_unique()
{
	local first filename cur usage options valopts compreplya compreplyb
        COMPREPLY=()
        _get_comp_words_by_ref -n = cur

# we need to make sure we run the correct program, not some other program 
# called unique which is located somewhere in $PATH
	first="${COMP_WORDS[0]}"
	filename=`echo  "${first}"|sed 's#^.*/\(.*\)$#\1#'`
	usage=`${first}|grep '^Usage:'|sed 's#^Usage:\? \?[^ ]*unique *##'`
	case "_${cur}" in
		_|_${first})
			if [[ "_${usage}" != "_OUTPUT-FILE" ]] ; then
				COMPREPLY=( $(compgen -W "${usage}" -- "") )
			fi
			return 0
			;;
		_-cut=*|_-mem=*)
			return 0
			;;
		_-inp=*|_-ex_file=*|_-ex_file_only=*)
                        cur=${cur#*=}
			__expand_tilde_by_ref cur 2>/dev/null
			_filedir
			return 0
			;;
		_-*)
			if [[ "_${usage}_" != "_OUTPUT-FILE_" ]] ; then
				options=`echo ${usage}|sed 's# #\n#g'|grep '^\[.*\]$'|sed 's#^.\(.*\).$#\1#'|sed 's#=.*$#=#'`
				valopts=`echo "${options}"|grep '='`
				compreplya=`compgen -W "${options}" -- ${cur}`
				compreplyb=`compgen -W "${valopts}" -- ${cur}`
				if [[ "_${compreplya}" == "_${compreplyb}" ]] ; then
					COMPREPLY=( $(compgen -W "${valopts}" -- "${cur}") )
					compopt -o nospace
				else
					COMPREPLY=( $(compgen -W "${options}" -- "${cur}") )
				fi
			fi
			return 0
			;;
		_*)
			return 0
			;;
	esac
} &&
complete -F _unique unique

Powered by blists - more mailing lists

Your e-mail address:

Powered by Openwall GNU/*/Linux - Powered by OpenVZ