diff --git a/README.md b/README.md index 3670cede53..c3362ad7c8 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ with the following contents and initialized as a git repository. * `fpm build` – build your library, executables and tests * `fpm run` – run executables * `fpm test` – run tests +* `fpm search` – search the local or remote registry * `fpm install` - installs the executables locally The command `fpm run` can optionally accept the name of the specific executable @@ -117,6 +118,12 @@ to run, as can `fpm test`; like `fpm run specific_executable`. Command line arguments can also be passed to the executable(s) or test(s) with the option `-- some arguments`. +Sample queries: + +* `fpm search --query mpi` +* `fpm search --namespace fortran-lang --package stdlib` +* `fpm search --license MIT --sort-by name --sort asc` + See additional instructions in the [Packaging guide](PACKAGING.md) or the [manifest reference](https://fpm.fortran-lang.org/spec/manifest.html). diff --git a/app/main.f90 b/app/main.f90 index 4bd3ac5e33..bf11e72a9e 100644 --- a/app/main.f90 +++ b/app/main.f90 @@ -11,7 +11,8 @@ program main fpm_update_settings, & fpm_clean_settings, & fpm_publish_settings, & - get_command_line_settings + get_command_line_settings, & + fpm_search_settings use fpm_error, only: error_t use fpm_filesystem, only: exists, parent_dir, join_path use fpm, only: cmd_build, cmd_run, cmd_clean @@ -20,6 +21,7 @@ program main use fpm_cmd_new, only: cmd_new use fpm_cmd_update, only : cmd_update use fpm_cmd_publish, only: cmd_publish +use fpm_cmd_search, only: cmd_search use fpm_os, only: change_directory, get_current_directory implicit none @@ -86,6 +88,8 @@ program main call cmd_update(settings) type is (fpm_clean_settings) call cmd_clean(settings) +type is (fpm_search_settings) + call cmd_search(settings) type is (fpm_publish_settings) call cmd_publish(settings) end select diff --git a/src/fpm/cmd/search.f90 b/src/fpm/cmd/search.f90 new file mode 100644 index 0000000000..331579414b --- /dev/null +++ b/src/fpm/cmd/search.f90 @@ -0,0 +1,220 @@ +!> Search packages in both the local cache and the remote registry using the +!> `search` subcommand. +!> Packages can be filtered by package name, namespace, version, query, +!> license, page, limit, and sort order. Use `--registry` to target a custom +!> registry URL. +!> +!> Sample queries: +!> fpm search --query mpi +!> fpm search --namespace fortran-lang --package stdlib +!> fpm search --license MIT --sort-by name --sort asc +!> fpm search --query hdf5 --page 1 --limit 10 --sort-by downloads --sort desc +module fpm_cmd_search + use fpm_command_line, only: fpm_search_settings + use fpm_downloader, only: downloader_t + use fpm_error, only: error_t, fatal_error, fpm_stop + use fpm_filesystem, only: basename, dirname, get_temp_filename, list_files + use fpm_settings, only: fpm_global_settings, get_global_settings + use fpm_strings, only: glob, string_t + use fpm_versioning, only: version_t + use jonquil, only: json_object + use tomlf, only: get_value, len, toml_array, toml_loads, toml_table + use tomlf_utils_io, only: read_whole_file + + implicit none + private + public :: cmd_search + + contains + + !> Search packages in the registry and in the local cache. + subroutine cmd_search(settings) + class(fpm_search_settings), intent(in) :: settings + + type(fpm_global_settings) :: global_settings + type(error_t), allocatable :: error + class(downloader_t), allocatable :: downloader + type(json_object) :: json + type(version_t), allocatable :: version + character(len=:), allocatable :: query_url, tmp_file + integer :: stat, unit + + call get_global_settings(global_settings, error) + if (allocated(error)) then + call fpm_stop(1, 'Error retrieving global settings') + return + end if + + tmp_file = get_temp_filename() + open (newunit=unit, file=tmp_file, action='readwrite', iostat=stat) + if (stat /= 0) then + call fpm_stop(1, 'Error creating temporary file for downloading package data.') + return + end if + + allocate (downloader) + query_url = build_query_url(settings) + call downloader%get_pkg_data(query_url, version, tmp_file, json, error) + close (unit, status='delete') + if (allocated(error)) then + call fpm_stop(1, 'Error retrieving package data from registry: '//trim(settings%registry)) + return + end if + + call print_local_matches(global_settings, settings, error) + if (allocated(error)) then + call fpm_stop(1, error%message) + return + end if + + call print_remote_matches(json, error) + if (allocated(error)) then + call fpm_stop(1, error%message) + end if + end subroutine cmd_search + + pure function build_query_url(settings) result(query_url) + class(fpm_search_settings), intent(in) :: settings + character(len=:), allocatable :: query_url + + query_url = trim(settings%registry)//'/packages_cli' & + // '?query='//trim(settings%query) & + // '&page='//trim(settings%page) & + // '&package='//trim(settings%package) & + // '&namespace='//trim(settings%namespace) & + // '&version='//trim(settings%version) & + // '&license='//trim(settings%license) & + // '&limit='//trim(settings%limit) & + // '&sort_by='//trim(settings%sort_by) & + // '&sort='//trim(settings%sort) + end function build_query_url + + subroutine print_local_matches(global_settings, settings, error) + type(fpm_global_settings), intent(in) :: global_settings + class(fpm_search_settings), intent(in) :: settings + type(error_t), allocatable, intent(out) :: error + + type(string_t), allocatable :: file_names(:) + type(toml_table), allocatable :: table + character(len=:), allocatable :: file_name, manifest_text, description, package_name + character(len=:), allocatable :: namespace, version, license + integer :: i, stat + + if (.not. allocated(global_settings%registry_settings)) then + call fatal_error(error, 'Registry settings are not available.') + return + end if + + if (.not. allocated(global_settings%registry_settings%cache_path)) then + call fatal_error(error, 'Registry cache path is not available.') + return + end if + + call list_files(global_settings%registry_settings%cache_path, file_names, recurse=.true.) + + print * + print *, 'Searching packages in local registry:' + print * + + do i = 1, size(file_names) + file_name = file_names(i)%s + if (basename(file_name) /= 'fpm.toml') cycle + + namespace = basename(dirname(dirname(dirname(file_name)))) + package_name = basename(dirname(dirname(file_name))) + version = basename(dirname(file_name)) + + if (.not. matches_pattern(namespace, settings%namespace)) cycle + if (.not. matches_pattern(package_name, settings%package)) cycle + if (.not. matches_pattern(version, settings%version)) cycle + + call read_whole_file(file_name, manifest_text, stat) + if (stat /= 0) then + call fatal_error(error, 'Error reading file: '//file_name) + return + end if + + call toml_loads(table, manifest_text) + if (.not. allocated(table)) then + call fatal_error(error, 'Error loading TOML file: '//file_name) + return + end if + + description = '' + license = '' + call get_value(table, 'description', description, stat=stat) + call get_value(table, 'license', license, stat=stat) + + if (matches_pattern(description, settings%query) .and. matches_pattern(license, settings%license)) then + call print_package_summary(package_name, namespace, version, description) + end if + end do + end subroutine print_local_matches + + subroutine print_remote_matches(json, error) + type(json_object), intent(inout) :: json + type(error_t), allocatable, intent(out) :: error + + type(toml_array), pointer :: packages + type(json_object), pointer :: package + character(len=:), allocatable :: name, namespace, description, version + integer :: i, stat + + if (.not. json%has_key('packages')) then + call fatal_error(error, 'Invalid package data returned') + return + end if + + call get_value(json, 'packages', packages) + if (.not. associated(packages)) then + call fatal_error(error, 'Invalid package data returned') + return + end if + + print * + print *, 'Searching packages in remote registry:' + print * + print '(A,I0,A)', ' Found ', len(packages), ' packages in fpm - registry:' + print * + + do i = 1, len(packages) + call get_value(packages, i, package) + if (.not. associated(package)) cycle + + name = '' + namespace = '' + description = '' + version = '' + call get_value(package, 'name', name, stat=stat) + call get_value(package, 'namespace', namespace, stat=stat) + call get_value(package, 'description', description, stat=stat) + call get_value(package, 'version', version, stat=stat) + + call print_package_summary(name, namespace, version, description) + end do + end subroutine print_remote_matches + + logical function matches_pattern(value, pattern) result(match) + character(*), intent(in) :: value + character(len=:), allocatable, intent(in) :: pattern + + if (.not. allocated(pattern)) then + match = .true. + else if (len_trim(pattern) == 0) then + match = .true. + else + match = glob(value, pattern) + end if + end function matches_pattern + + subroutine print_package_summary(name, namespace, version, description) + character(len=*), intent(in) :: name, namespace, version, description + + print *, 'Name: ', name + print *, 'Namespace: ', namespace + print *, 'Version: ', version + print *, 'Description: ', description + print * + end subroutine print_package_summary + end module fpm_cmd_search + \ No newline at end of file diff --git a/src/fpm/downloader.f90 b/src/fpm/downloader.f90 index 4c19bf9f29..fc01a8967f 100644 --- a/src/fpm/downloader.f90 +++ b/src/fpm/downloader.f90 @@ -62,10 +62,10 @@ subroutine get_file(url, tmp_pkg_file, error) if (which('curl') /= '') then print *, "Downloading '"//url//"' -> '"//tmp_pkg_file//"'" - call execute_command_line('curl '//url//' -s -o '//tmp_pkg_file, exitstat=stat) + call execute_command_line('curl "'//url//'" -s -o '//tmp_pkg_file, exitstat=stat) else if (which('wget') /= '') then print *, "Downloading '"//url//"' -> '"//tmp_pkg_file//"'" - call execute_command_line('wget '//url//' -q -O '//tmp_pkg_file, exitstat=stat) + call execute_command_line('wget "'//url//'" -q -O '//tmp_pkg_file, exitstat=stat) else call fatal_error(error, "Neither 'curl' nor 'wget' installed."); return end if diff --git a/src/fpm_command_line.f90 b/src/fpm_command_line.f90 index 44e13b5115..9fe8004ecf 100644 --- a/src/fpm_command_line.f90 +++ b/src/fpm_command_line.f90 @@ -32,6 +32,7 @@ module fpm_command_line string_t, glob, is_valid_feature_name use fpm_filesystem, only : basename, canon_path, which, run use fpm_environment, only : get_command_arguments_quoted +use fpm_settings, only : official_registry_base_url use fpm_error, only : fpm_stop, error_t use fpm_os, only : get_current_directory use fpm_release, only : fpm_version, version_t @@ -52,6 +53,7 @@ module fpm_command_line fpm_clean_settings, & fpm_publish_settings, & get_command_line_settings, & + fpm_search_settings, & get_fpm_env type, abstract :: fpm_cmd_settings @@ -139,6 +141,20 @@ module fpm_command_line logical :: registry_cache = .false. end type +!> Settings for searching for packages in local and remote registries +type, extends(fpm_cmd_settings) :: fpm_search_settings + character(len=:),allocatable :: query !> search for packages with a specific query (globbing supported) + character(len=:),allocatable :: page !> return in a specific page of results of remote registry (default: 1) + character(len=:),allocatable :: registry !> search in a specific registry (default: official registry), stores the URL of the registry + character(len=:),allocatable :: namespace !> search for packages with a specific namespace (globbing supported) + character(len=:),allocatable :: package !> search for packages with a specific name (globbing supported) + character(len=:),allocatable :: version !> search for packages with version (globbing supported) + character(len=:),allocatable :: license !> search for packages with a specific license (globbing supported) + character(len=:),allocatable :: limit !> limit the number of results returned (default: 10). + character(len=:),allocatable :: sort_by !> sort the results by name, author, createdat, updatedat, downloads (default: name) + character(len=:),allocatable :: sort !> sort the results in ascending or descending (asc or desc) order (default: asc). +end type + type, extends(fpm_build_settings) :: fpm_publish_settings logical :: show_package_version = .false. logical :: show_upload_data = .false. @@ -156,9 +172,9 @@ module fpm_command_line & help_test(:), help_build(:), help_usage(:), help_runner(:), & & help_text(:), help_install(:), help_help(:), help_update(:), & & help_list(:), help_list_dash(:), help_list_nodash(:), & - & help_clean(:), help_publish(:) + & help_clean(:), help_publish(:), help_search(:) character(len=20),parameter :: manual(*)=[ character(len=20) ::& -& ' ', 'fpm', 'new', 'build', 'run', 'clean', & +& ' ', 'fpm', 'new', 'build', 'run', 'clean', 'search', & & 'test', 'runner', 'install', 'update', 'list', 'help', 'version', 'publish' ] character(len=:), allocatable :: val_runner,val_runner_args,val_dump @@ -250,7 +266,8 @@ subroutine get_command_line_settings(cmd_settings) type(fpm_export_settings) , allocatable :: export_settings type(version_t) :: version character(len=:), allocatable :: common_args, compiler_args, run_args, working_dir, & - & c_compiler, cxx_compiler, archiver, version_s, token_s, config_file + & c_compiler, cxx_compiler, archiver, version_s, token_s, config_file, query, & + & page, registry, namespace, license, package, package_version, limit, sort_by, sort character(len=*), parameter :: fc_env = "FC", cc_env = "CC", ar_env = "AR", & & fflags_env = "FFLAGS", cflags_env = "CFLAGS", cxxflags_env = "CXXFLAGS", & @@ -514,6 +531,8 @@ subroutine get_command_line_settings(cmd_settings) help_text=[character(len=widest) :: help_text, version_text] case('clean' ) help_text=[character(len=widest) :: help_text, help_clean] + case('search' ) + help_text=[character(len=widest) :: help_text, help_search] case('publish') help_text=[character(len=widest) :: help_text, help_publish] case default @@ -704,6 +723,56 @@ subroutine get_command_line_settings(cmd_settings) end select end block + case('search') + call set_args(common_args //'& + & --query " " & + & --page " " & + & --registry " " & + & --namespace " " & + & --package " " & + & --package_version " " & + & --license " " & + & --limit " " & + & --sort-by " " & + & --sort " " & + & --', help_search, version_text) + + query = sget('query') + namespace = sget('namespace') + package = sget('package') + package_version = sget('package_version') + license = sget('license') + registry = sget('registry') + page = sget('page') + limit = sget('limit') + sort_by = sget('sort-by') + sort = sget('sort') + + block + + if (query==' ') query='' + if (page==' ') page='1' + if (package==' ') package='*' + if (license==' ') license='' + if (sort_by==' ') sort_by='name' + if (sort==' ') sort='asc' + if (limit==' ') limit='10' + if (namespace==' ') namespace='*' + if (package_version==' ') package_version='*' + if (.not. registry=='') then + print *, 'Using custom registry for searching packages: ', registry + registry = trim(adjustl(registry)) + else + registry = official_registry_base_url + end if + allocate(fpm_search_settings :: cmd_settings) + cmd_settings = fpm_search_settings( & + & query=query, page=page, registry=registry, & + & namespace=namespace, package=package, version=package_version, & + & license=license, limit=limit, sort_by=sort_by, & + & sort=sort) + end block + case('publish') call set_args(common_args // compiler_args //'& & --show-package-version F & @@ -831,6 +900,7 @@ subroutine set_help() ' update Update and manage project dependencies ', & ' install Install project ', & ' clean Delete the build ', & + ' search Search for the packages in local registry and fpm-registry ', & ' publish Publish package to the registry ', & ' ', & ' Enter "fpm --list" for a brief list of subcommand options. Enter ', & @@ -852,10 +922,12 @@ subroutine set_help() ' test [[--target] NAME(s)] [--profile PROF] [--features LIST] [--flag FFLAGS] ', & ' [--runner "CMD"] [--list] [--compiler COMPILER_NAME] [--config-file PATH] ', & ' [-- ARGS] ', & - ' install [--profile PROF] [--flag FFLAGS] [--no-rebuild] [--prefix PATH] ', & - ' [--config-file PATH] [--registry-cache] [options] ', & - ' clean [--skip|--all] [--test] [--apps] [--examples] [--config-file PATH] ', & - ' [--registry-cache] ', & + ' [--config-file PATH] [--registry-cache] [options] ', & + ' clean [--skip|--all] [--test] [--apps] [--examples] [--config-file PATH] ', & + ' [--registry-cache] ', & + ' search [--query query] [--page page] [--registry URL] [--namespace namespace] ', & + ' [--package package] [--package_version version] [--license license] ', & + ' [--limit <10>] [--sort-by ] [--sort ] ', & ' publish [--token TOKEN] [--show-package-version] [--show-upload-data] ', & ' [--dry-run] [--verbose] [--config-file PATH] ', & ' '] @@ -966,6 +1038,7 @@ subroutine set_help() ' + install Install project. ', & ' + clean Delete directories in the "build/" directory, except ', & ' dependencies. Use --test/--apps/--examples for selective. ', & + ' + search Search for packages in local and fpm-registry ', & ' + publish Publish package to the registry. ', & ' ', & ' Their syntax is ', & @@ -988,6 +1061,9 @@ subroutine set_help() ' [options] [--config-file PATH] [--registry-cache] ', & ' clean [--skip|--all] [--test] [--apps] [--examples] [--config-file PATH] ', & ' [--registry-cache] ', & + ' search [--query query] [--page page] [--registry URL] [--namespace namespace] ', & + ' [--package package] [--package_version version] [--license license] ', & + ' [--limit <10>] [--sort-by ] [--sort ] ', & ' publish [--token TOKEN] [--show-package-version] [--show-upload-data] ', & ' [--dry-run] [--verbose] [--config-file PATH] ', & ' ', & @@ -1059,6 +1135,7 @@ subroutine set_help() ' fpm run myprogram --profile release -- -x 10 -y 20 --title "my title" ', & ' fpm install --prefix ~/.local ', & ' fpm clean --all ', & + ' fpm search --query fortran --page 2 ', & ' ', & 'SEE ALSO ', & ' ', & @@ -1503,6 +1580,32 @@ subroutine set_help() ' --config-file PATH Custom location of the global config file.', & ' --registry-cache Delete registry cache.', & '' ] + help_search=[character(len=80) :: & + 'NAME', & + ' search(1) - search for the package in local and fpm - registry.', & + '', & + 'SYNOPSIS', & + ' fpm search', & + '', & + 'DESCRIPTION', & + ' Search for packages in the local directory and the fpm-registry, ', & + ' supports package search by name, namespace, query (description and README.md)', & + ' and license from the registries (local and remote).', & + '', & + 'OPTIONS', & + ' --query Search for any term, can be used for searching across parameters like:', & + ' name, namespace, description, and license, version, keywords, ', & + ' README, maintainer, author. (supports globbing)', & + ' --page Page number for results.', & + ' --registry URL of the registry to query.', & + ' --namespace Namespace of the package', & + ' --package Package name to filter results.', & + ' --package_version Version of the package', & + ' --license License type to filter results.', & + ' --limit Maximum number of results to return.', & + ' --sort-by Field to sort results by (e.g., name).', & + ' --sort Sort order (asc for ascending, desc for descending).', & + '' ] help_publish=[character(len=80) :: & 'NAME', & ' publish(1) - publish package to the registry', & @@ -1516,7 +1619,7 @@ subroutine set_help() 'DESCRIPTION', & ' Follow the steps to create a tarball and upload a package to the registry:', & '', & - ' 1. Register on the website (https://registry-phi.vercel.app/).', & + ' 1. Register on the website (TODO: registry url).', & ' 2. Create a namespace. Uploaded packages must be assigned to a unique', & ' namespace to avoid conflicts among packages with similar names. A', & ' namespace can accommodate multiple packages.', & @@ -1589,9 +1692,9 @@ function runner_command(cmd) result(run_cmd) end function runner_command !> Check name in list ID. return 0 if not found - integer function name_ID(cmd,name) + integer function name_ID(cmd,pattern) class(fpm_run_settings), intent(in) :: cmd - character(*), intent(in) :: name + character(*), intent(in) :: pattern integer :: j @@ -1601,7 +1704,7 @@ integer function name_ID(cmd,name) do j=1,size(cmd%name) - if (glob(trim(name),trim(cmd%name(j)))) then + if (glob(trim(pattern),trim(cmd%name(j)))) then name_ID = j return end if diff --git a/src/fpm_settings.f90 b/src/fpm_settings.f90 index ee1af41f4f..68dc9e015a 100644 --- a/src/fpm_settings.f90 +++ b/src/fpm_settings.f90 @@ -5,7 +5,7 @@ module fpm_settings use fpm_error, only: error_t, fatal_error use tomlf, only: toml_table, toml_error, toml_stat, toml_load use fpm_toml, only: get_value, check_keys - use fpm_os, only: get_current_directory, change_directory, get_absolute_path, convert_to_absolute_path + use fpm_os, only: get_absolute_path, convert_to_absolute_path implicit none private @@ -114,7 +114,8 @@ subroutine get_global_settings(global_settings, error) subroutine use_default_registry_settings(global_settings) type(fpm_global_settings), intent(inout) :: global_settings - if (.not. allocated(global_settings%registry_settings)) allocate (global_settings%registry_settings) + if (allocated(global_settings%registry_settings)) deallocate (global_settings%registry_settings) + allocate (global_settings%registry_settings) global_settings%registry_settings%url = official_registry_base_url global_settings%registry_settings%cache_path = join_path(global_settings%path_to_config_folder_or_empty(), & & 'dependencies') @@ -132,7 +133,7 @@ subroutine get_registry_settings(table, global_settings, error) character(:), allocatable :: path, url, cache_path integer :: stat - !> List of valid keys for the dependency table. + !> List of valid keys for the registry table. character(*), dimension(*), parameter :: valid_keys = [character(10) :: & & 'path', & & 'url', & @@ -142,12 +143,13 @@ subroutine get_registry_settings(table, global_settings, error) call check_keys(table, valid_keys, error) if (allocated(error)) return + if (allocated(global_settings%registry_settings)) deallocate (global_settings%registry_settings) allocate (global_settings%registry_settings) if (table%has_key('path')) then call get_value(table, 'path', path, stat=stat) if (stat /= toml_stat%success) then - call fatal_error(error, "Error reading registry path: '"//path//"'."); return + call fatal_error(error, "Error reading registry path from config file."); return end if end if @@ -171,7 +173,7 @@ subroutine get_registry_settings(table, global_settings, error) if (table%has_key('url')) then call get_value(table, 'url', url, stat=stat) if (stat /= toml_stat%success) then - call fatal_error(error, "Error reading registry url: '"//url//"'."); return + call fatal_error(error, "Error reading registry url from config file."); return end if end if @@ -188,7 +190,7 @@ subroutine get_registry_settings(table, global_settings, error) if (table%has_key('cache_path')) then call get_value(table, 'cache_path', cache_path, stat=stat) if (stat /= toml_stat%success) then - call fatal_error(error, "Error reading path to registry cache: '"//cache_path//"'."); return + call fatal_error(error, "Error reading path to registry cache from config file."); return end if end if diff --git a/test/cli_test/cli_test.f90 b/test/cli_test/cli_test.f90 index 7df098b2cb..b8a2f33d64 100644 --- a/test/cli_test/cli_test.f90 +++ b/test/cli_test/cli_test.f90 @@ -41,14 +41,24 @@ program main character(len=:), allocatable :: profile,act_profile ; namelist/act_cli/act_profile character(len=:), allocatable :: args,act_args ; namelist/act_cli/act_args -namelist/expected/cmd,cstat,estat,w_e,w_t,c_s,c_a,c_t,c_apps,c_ex,reg_c,name,features,profile,args,show_v,show_u_d,dry_run,token +character(len=:), allocatable :: query,act_query ; namelist/act_cli/act_query +character(len=:), allocatable :: page,act_page ; namelist/act_cli/act_page +character(len=:), allocatable :: registry,act_registry; namelist/act_cli/act_registry +character(len=:), allocatable :: namespace,act_namespace ; namelist/act_cli/act_namespace +character(len=:), allocatable :: package_version,act_package_version ; namelist/act_cli/act_package_version +character(len=:), allocatable :: license,act_license ; namelist/act_cli/act_license +character(len=:), allocatable :: limit,act_limit ; namelist/act_cli/act_limit +character(len=:), allocatable :: sort_by,act_sort_by ; namelist/act_cli/act_sort_by +character(len=:), allocatable :: sort,act_sort ; namelist/act_cli/act_sort +namelist/expected/cmd,cstat,estat,w_e,w_t,c_s,c_a,c_t,c_apps,c_ex,reg_c,name,features,profile,args,query,page,registry,namespace,& + package_version,license,limit,sort_by,sort,show_v,show_u_d,dry_run,token integer :: lun logical,allocatable :: tally(:) logical,allocatable :: subtally(:) character(len=256) :: message ! table of arguments to pass to program and expected non-default values for that execution in NAMELIST group format -character(len=*),parameter :: tests(*)= [ character(len=256) :: & +character(len=*),parameter :: tests(*)= [ character(len=512) :: & 'CMD="new", ESTAT=1,', & !'CMD="new -unknown", ESTAT=2,', & @@ -74,6 +84,16 @@ program main 'CMD="test proj1 p2 project3 --profile release -- arg1 -x ""and a long one""", & &NAME="proj1","p2","project3",profile="release",ARGS="""arg1"" ""-x"" ""and a long one""", ', & +'CMD="search", QUERY="", PAGE="1", REGISTRY="https://github.com/fortran-lang/' // & +'fpm-registry", NAMESPACE="*", PACKAGE_VERSION="*", LICENSE="", ' // & +'LIMIT="10", SORT_BY="name", SORT="asc",', & +'CMD="search --query mpi --registry https://example.org --namespace fortran-lang ' // & +'--package_version 0.1.0 --license MIT --page 2 --limit 25 ' // & +'--sort-by downloads --sort desc", & + &QUERY="mpi", PAGE="2", REGISTRY="https://example.org", NAMESPACE="fortran-lang", ' // & + &'PACKAGE_VERSION="0.1.0", LICENSE="MIT", LIMIT="25", ' // & + &'SORT_BY="downloads", SORT="desc",', & + 'CMD="build", NAME=, profile="",features=,ARGS="",', & 'CMD="build --profile release", NAME=, profile="release",features=,ARGS="",', & 'CMD="build --features debug,mpi", NAME=, profile="",features="debug","mpi",ARGS="",', & @@ -93,7 +113,7 @@ program main 'CMD="publish --token abc --dry-run", DRY_RUN=T, NAME=, token="abc",ARGS="",', & 'CMD="publish --token abc", NAME=, token="abc",ARGS="",', & ' ' ] -character(len=256) :: readme(3) +character(len=512) :: readme(3) readme(1)='&EXPECTED' ! top and bottom line for a NAMELIST group read from TEST() used to set the expected values readme(3)=' /' @@ -131,8 +151,17 @@ program main show_u_d=.false. ! --show-upload-data dry_run=.false. ! --dry-run token='' ! --token TOKEN - args=repeat(' ',132) ! -- ARGS - cmd=repeat(' ',132) ! the command line arguments to test + args=repeat(' ',512) ! -- ARGS + query='' ! --query + page='' ! --page + registry='' ! --registry + namespace='' ! --namespace + package_version='' ! --package_version + license='' ! --license + limit='' ! --limit + sort_by='' ! --sort-by + sort='' ! --sort + cmd=repeat(' ',512) ! the command line arguments to test cstat=0 ! status values from EXECUTE_COMMAND_LINE() estat=0 readme(2)=' '//tests(i) ! select command options to test for CMD and set nondefault expected values @@ -157,7 +186,16 @@ program main act_show_u_d=.false. act_dry_run=.false. act_token='' - act_args=repeat(' ',132) + act_args=repeat(' ',512) + act_query='' + act_page='' + act_registry='' + act_namespace='' + act_package_version='' + act_license='' + act_limit='' + act_sort_by='' + act_sort='' read(lun,nml=act_cli,iostat=ios,iomsg=message) if(ios/=0)then write(*,'(a)')'ERROR:',trim(message) @@ -179,6 +217,15 @@ program main call test_test('DRY-RUN',act_dry_run.eqv.dry_run) call test_test('TOKEN',act_token==token) call test_test('ARGS',act_args==args) + call test_test('QUERY',act_query==query) + call test_test('PAGE',act_page==page) + call test_test('REGISTRY',act_registry==registry) + call test_test('NAMESPACE',act_namespace==namespace) + call test_test('PACKAGE_VERSION',act_package_version==package_version) + call test_test('LICENSE',act_license==license) + call test_test('LIMIT',act_limit==limit) + call test_test('SORT_BY',act_sort_by==sort_by) + call test_test('SORT',act_sort==sort) if(all(subtally))then write(*,'(*(g0))')'PASSED: TEST ',i,' STATUS: expected ',cstat,' ',estat,' actual ',act_cstat,' ',act_estat,& & ' for [',trim(cmd),']' @@ -231,10 +278,10 @@ program main contains -subroutine test_test(name,tst) -character(len=*) :: name +subroutine test_test(label,tst) +character(len=*) :: label logical,intent(in) :: tst - !!write(*,'(*(g0,1x))')' SUBTEST ',name,' ',merge('PASSED','FAILED',tst) + !!write(*,'(*(g0,1x))')' SUBTEST ',label,' ',merge('PASSED','FAILED',tst) subtally=[subtally,tst] end subroutine test_test @@ -248,6 +295,7 @@ subroutine parse() fpm_test_settings, & fpm_clean_settings, & fpm_install_settings, & + fpm_search_settings, & get_command_line_settings, & fpm_publish_settings use fpm, only: cmd_run, cmd_clean @@ -272,6 +320,15 @@ subroutine parse() act_dry_run=.false. act_token='' act_profile='' +act_query='' +act_page='' +act_registry='' +act_namespace='' +act_package_version='' +act_license='' +act_limit='' +act_sort_by='' +act_sort='' select type(settings=>cmd_settings) type is (fpm_new_settings) @@ -316,6 +373,16 @@ subroutine parse() act_show_u_d=settings%show_upload_data act_dry_run=settings%is_dry_run act_token=settings%token +type is (fpm_search_settings) + if (allocated(settings%query)) act_query=settings%query + if (allocated(settings%page)) act_page=settings%page + if (allocated(settings%registry)) act_registry=settings%registry + if (allocated(settings%namespace)) act_namespace=settings%namespace + if (allocated(settings%version)) act_package_version=settings%version + if (allocated(settings%license)) act_license=settings%license + if (allocated(settings%limit)) act_limit=settings%limit + if (allocated(settings%sort_by)) act_sort_by=settings%sort_by + if (allocated(settings%sort)) act_sort=settings%sort end select open(file='_test_cli',newunit=lun,delim='quote') diff --git a/test/help_test/help_test.f90 b/test/help_test/help_test.f90 index 6210a51bfb..b36d1c6e50 100644 --- a/test/help_test/help_test.f90 +++ b/test/help_test/help_test.f90 @@ -34,6 +34,7 @@ program help_test 'help list >> fpm_scratch_help.txt',& 'help help >> fpm_scratch_help.txt',& 'help clean >> fpm_scratch_help.txt',& +'help search >> fpm_scratch_help.txt',& 'help publish >> fpm_scratch_help.txt',& '--version >> fpm_scratch_help.txt',& ! generate manual @@ -43,7 +44,7 @@ program help_test !'fpm run -- --list >> fpm_scratch_help.txt',& !'fpm run -- list --list >> fpm_scratch_help.txt',& character(len=*),parameter :: names(*)=[character(len=10) ::& - 'fpm','new','update','build','run','test','runner','install','list','help','clean'] + 'fpm','new','update','build','run','test','runner','install','list','help','clean','search'] character(len=:), allocatable :: prog integer :: length