our
$AUTHORITY
=
'cpan:PERLANCAR'
;
our
$DATE
=
'2021-02-02'
;
our
$DIST
=
'Complete-Path'
;
our
$VERSION
=
'0.251'
;
use
5.010001;
our
$COMPLETE_PATH_TRACE
=
$ENV
{COMPLETE_PATH_TRACE} // 0;
our
@ISA
=
qw(Exporter)
;
our
@EXPORT_OK
=
qw(
complete_path
)
;
sub
_dig_leaf {
my
(
$p
,
$list_func
,
$is_dir_func
,
$filter_func
,
$path_sep
) =
@_
;
my
$num_dirs
;
my
$listres
=
$list_func
->(
$p
,
''
, 0);
return
$p
unless
ref
(
$listres
) eq
'ARRAY'
&&
@$listres
;
my
@candidates
;
L1:
for
my
$e
(
@$listres
) {
my
$p2
=
$p
=~ m!\Q
$path_sep
\E\z! ?
"$p$e"
:
"$p$path_sep$e"
;
{
local
$_
=
$p2
;
next
L1
if
$filter_func
&& !
$filter_func
->(
$p2
);
}
push
@candidates
,
$p2
;
}
return
$p
unless
@candidates
== 1;
my
$p2
=
$candidates
[0];
my
$is_dir
;
if
(
$p2
=~ m!\Q
$path_sep
\E\z!) {
$is_dir
++;
}
else
{
$is_dir
=
$is_dir_func
&&
$is_dir_func
->(
$p2
);
}
return
_dig_leaf(
$p2
,
$list_func
,
$is_dir_func
,
$filter_func
,
$path_sep
)
if
$is_dir
;
$p2
;
}
our
%SPEC
;
$SPEC
{complete_path} = {
v
=> 1.1,
summary
=>
'Complete path'
,
description
=>
<<'_',
Complete path, for anything path-like. Meant to be used as backend for other
functions like `Complete::File::complete_file` or
`Complete::Module::complete_module`. Provides features like case-insensitive
matching, expanding intermediate paths, and case mapping.
Algorithm is to split path into path elements, then list items (using the
supplied `list_func`) and perform filtering (using the supplied `filter_func`)
at every level.
_
args
=> {
%arg_word
,
list_func
=> {
summary
=>
'Function to list the content of intermediate "dirs"'
,
schema
=>
'code*'
,
req
=> 1,
description
=>
<<'_',
Code will be called with arguments: ($path, $cur_path_elem, $is_intermediate).
Code should return an arrayref containing list of elements. "Directories" can be
marked by ending the name with the path separator (see `path_sep`). Or, you can
also provide an `is_dir_func` function that will be consulted after filtering.
If an item is a "directory" then its name will be suffixed with a path
separator by `complete_path()`.
_
},
is_dir_func
=> {
summary
=>
'Function to check whether a path is a "dir"'
,
schema
=>
'code*'
,
description
=>
<<'_',
Optional. You can provide this function to determine if an item is a "directory"
(so its name can be suffixed with path separator). You do not need to do this if
you already suffix names of "directories" with path separator in `list_func`.
One reason you might want to provide this and not mark "directories" in
`list_func` is when you want to do extra filtering with `filter_func`. Sometimes
you do not want to suffix the names first (example: see `complete_file` in
`Complete::File`).
_
},
starting_path
=> {
schema
=>
'str*'
,
req
=> 1,
default
=>
''
,
},
filter_func
=> {
schema
=>
'code*'
,
description
=>
<<'_',
Provide extra filtering. Code will be given path and should return 1 if the item
should be included in the final result or 0 if the item should be excluded.
_
},
path_sep
=> {
schema
=>
'str*'
,
default
=>
'/'
,
},
recurse
=> {
schema
=>
'bool*'
,
cmdline_aliases
=> {
r
=>{}},
},
recurse_matching
=> {
schema
=> [
'str*'
,
in
=>[
'level-by-level'
,
'all-at-once'
]],
default
=>
'level-by-level'
,
},
exclude_leaf
=> {
schema
=>
'bool*'
,
},
exclude_dir
=> {
schema
=>
'bool*'
,
},
},
args_rels
=> {
dep_all
=> [
recurse_matching
=> [
'recurse'
]],
},
result_naked
=> 1,
result
=> {
schema
=>
'array'
,
},
};
sub
complete_path {
my
%args
=
@_
;
my
$word
=
$args
{word} //
""
;
my
$path_sep
=
$args
{path_sep} //
'/'
;
my
$list_func
=
$args
{list_func};
my
$is_dir_func
=
$args
{is_dir_func};
my
$filter_func
=
$args
{filter_func};
my
$result_prefix
=
$args
{result_prefix};
my
$starting_path
=
$args
{starting_path} //
''
;
my
$recurse
=
$args
{recurse};
my
$recurse_matching
=
$args
{recurse_matching} //
'level-by-level'
;
my
$exclude_leaf
=
$args
{exclude_leaf};
my
$exclude_dir
=
$args
{exclude_dir};
my
$ci
=
$Complete::Common::OPT_CI
;
my
$word_mode
=
$Complete::Common::OPT_WORD_MODE
;
my
$fuzzy
=
$Complete::Common::OPT_FUZZY
;
my
$map_case
=
$Complete::Common::OPT_MAP_CASE
;
my
$exp_im_path
=
$Complete::Common::OPT_EXP_IM_PATH
;
my
$dig_leaf
=
$Complete::Common::OPT_DIG_LEAF
;
my
$re_ends_with_path_sep
=
qr!\A\z|\Q$path_sep\E\z!
;
my
@res
;
my
$cut_chars
;
if
(
defined
$args
{_cut_chars}) {
$cut_chars
=
$args
{_cut_chars};
}
else
{
$cut_chars
= 0;
if
(
length
(
$starting_path
)) {
$cut_chars
+=
length
(
$starting_path
);
unless
(
$starting_path
=~ /\Q
$path_sep
\E\z/) {
$cut_chars
+=
length
(
$path_sep
);
}
}
}
RECURSE_MATCHING_ALL_AT_ONCE: {
last
unless
$recurse
&&
$recurse_matching
eq
'all-at-once'
;
my
@dirs
= (
$starting_path
);
while
(
@dirs
) {
my
$dir
=
shift
@dirs
;
my
$listres
=
$list_func
->(
$dir
,
''
, 0);
next
unless
$listres
&&
@$listres
;
L1:
for
my
$e
(
@$listres
) {
my
$p
=
$dir
=~
$re_ends_with_path_sep
?
"$dir$e"
:
"$dir$path_sep$e"
;
{
local
$_
=
$p
;
next
L1
if
$filter_func
&& !
$filter_func
->(
$p
);
}
my
$is_dir
;
if
(
$e
=~
$re_ends_with_path_sep
) {
$is_dir
= 1;
}
else
{
local
$_
=
$p
;
$is_dir
=
$is_dir_func
->(
$p
);
}
if
(
$is_dir
) {
push
@dirs
,
$p
}
$p
=
"$result_prefix$p"
if
length
(
$result_prefix
);
substr
(
$p
, 0,
$cut_chars
) =
''
if
$cut_chars
;
unless
(
$p
=~ /\Q
$path_sep
\E\z/) {
$p
.=
$path_sep
if
$is_dir
;
}
push
@res
,
$p
unless
(
$is_dir
&&
$exclude_dir
) || (!
$is_dir
&&
$exclude_leaf
);
}
}
@res
= @{ Complete::Util::complete_array_elem(
array
=> \
@res
,
word
=>
$word
,
) };
goto
RETURN_RESULT;
}
my
@intermediate_dirs
;
{
@intermediate_dirs
=
split
qr/\Q$path_sep/
,
$word
;
@intermediate_dirs
= (
''
)
if
!
@intermediate_dirs
;
push
@intermediate_dirs
,
''
if
$word
=~
$re_ends_with_path_sep
;
}
my
$leaf
=
pop
@intermediate_dirs
;
@intermediate_dirs
= (
''
)
if
!
@intermediate_dirs
;
my
@candidate_paths
;
for
my
$i
(0..
$#intermediate_dirs
) {
my
$intdir
=
$intermediate_dirs
[
$i
];
my
$intdir_with_path_sep
=
"$intdir$path_sep"
;
my
@dirs
;
if
(
$i
== 0) {
@dirs
= (
$starting_path
);
}
else
{
@dirs
=
@candidate_paths
;
}
if
(
$i
==
$#intermediate_dirs
&&
$intdir
eq
''
) {
@candidate_paths
=
@dirs
;
last
;
}
my
@new_candidate_paths
;
for
my
$dir
(
@dirs
) {
my
$listres
=
$list_func
->(
$dir
,
$intdir
, 1);
next
unless
$listres
&&
@$listres
;
my
$matches
= Complete::Util::complete_array_elem(
word
=>
$intdir
,
array
=>
$listres
,
);
my
$exact_matches
= [
grep
{
$_
eq
$intdir
||
$_
eq
$intdir_with_path_sep
}
@$matches
];
if
(!
$exp_im_path
||
@$exact_matches
== 1) {
$matches
=
$exact_matches
;
}
for
(
@$matches
) {
my
$p
=
$dir
=~
$re_ends_with_path_sep
?
"$dir$_"
:
"$dir$path_sep$_"
;
push
@new_candidate_paths
,
$p
;
}
}
return
[]
unless
@new_candidate_paths
;
@candidate_paths
=
@new_candidate_paths
;
}
log_trace
"[comppath] candidate paths: %s"
, \
@candidate_paths
if
$ENV
{COMPLETE_PATH_TRACE};
for
my
$dir
(
@candidate_paths
) {
my
$listres
=
$list_func
->(
$dir
,
$leaf
, 0);
next
unless
$listres
&&
@$listres
;
my
$matches
= Complete::Util::complete_array_elem(
word
=>
$leaf
,
array
=>
$listres
,
);
L1:
for
my
$e
(
@$matches
) {
my
$p
=
$dir
=~
$re_ends_with_path_sep
?
"$dir$e"
:
"$dir$path_sep$e"
;
{
local
$_
=
$p
;
next
L1
if
$filter_func
&& !
$filter_func
->(
$p
);
}
my
$is_dir
;
if
(
$e
=~
$re_ends_with_path_sep
) {
$is_dir
= 1;
}
else
{
local
$_
=
$p
;
$is_dir
=
$is_dir_func
->(
$p
);
}
my
@subres
;
if
(
$is_dir
) {
if
(
$recurse
) {
@subres
= @{complete_path(
%args
,
starting_path
=>
$p
,
word
=>
''
,
_cut_chars
=>
$cut_chars
,
)};
}
elsif
(
$dig_leaf
) {
DIG_LEAF:
{
my
$p2
= _dig_leaf(
$p
,
$list_func
,
$is_dir_func
,
$filter_func
,
$path_sep
);
last
DIG_LEAF
if
$p2
eq
$p
;
$p
=
$p2
;
if
(
$p
=~
$re_ends_with_path_sep
) {
$is_dir
= 1;
}
else
{
local
$_
=
$p
;
$is_dir
=
$is_dir_func
->(
$p
);
}
}
}
}
my
$p0
=
$p
;
substr
(
$p
, 0,
$cut_chars
) =
''
if
$cut_chars
;
$p
=
"$result_prefix$p"
if
length
(
$result_prefix
);
unless
(
$p
=~ /\Q
$path_sep
\E\z/) {
$p
.=
$path_sep
if
$is_dir
;
}
push
@res
,
$p
unless
(
$is_dir
&&
$exclude_dir
) || (!
$is_dir
&&
$exclude_leaf
);
push
@res
,
@subres
;
}
}
RETURN_RESULT:
\
@res
;
}
1;