diff --git a/stdlib/Pkg/src/API.jl b/stdlib/Pkg/src/API.jl index 8d6d85fb284f0a..dc659e39594082 100644 --- a/stdlib/Pkg/src/API.jl +++ b/stdlib/Pkg/src/API.jl @@ -28,9 +28,11 @@ add_or_develop(pkg::Union{String, PackageSpec}; kwargs...) = add_or_develop([pkg add_or_develop(pkgs::Vector{String}; kwargs...) = add_or_develop([check_package_name(pkg) for pkg in pkgs]; kwargs...) add_or_develop(pkgs::Vector{PackageSpec}; kwargs...) = add_or_develop(Context(), pkgs; kwargs...) -function add_or_develop(ctx::Context, pkgs::Vector{PackageSpec}; mode::Symbol, devdir::Union{String,Nothing}=nothing, kwargs...) +function add_or_develop(ctx::Context, pkgs::Vector{PackageSpec}; mode::Symbol, devdir::Bool=false, kwargs...) Context!(ctx; kwargs...) + devdir = devdir ? joinpath(dirname(ctx.env.project_file), "dev") : nothing + # All developed packages should go through handle_repos_develop so just give them an empty repo for pkg in pkgs mode == :develop && pkg.repo == nothing && (pkg.repo = Types.GitRepo()) @@ -67,7 +69,12 @@ rm(pkg::Union{String, PackageSpec}; kwargs...) = rm([pkg]; kwargs...) rm(pkgs::Vector{String}; kwargs...) = rm([PackageSpec(pkg) for pkg in pkgs]; kwargs...) rm(pkgs::Vector{PackageSpec}; kwargs...) = rm(Context(), pkgs; kwargs...) -function rm(ctx::Context, pkgs::Vector{PackageSpec}; kwargs...) +function rm(ctx::Context, pkgs::Vector{PackageSpec}; mode=PKGMODE_PROJECT, kwargs...) + for pkg in pkgs + #TODO only overwrite pkg.mode is default value ? + pkg.mode = mode + end + Context!(ctx; kwargs...) ctx.preview && preview_info() project_deps_resolve!(ctx.env, pkgs) @@ -144,6 +151,12 @@ up(pkgs::Vector{PackageSpec}; kwargs...) = up(Context(), pkgs; kwargs...) function up(ctx::Context, pkgs::Vector{PackageSpec}; level::UpgradeLevel=UPLEVEL_MAJOR, mode::PackageMode=PKGMODE_PROJECT, do_update_registry=true, kwargs...) + for pkg in pkgs + # TODO only override if they are not already set + pkg.mode = mode + pkg.version = level + end + Context!(ctx; kwargs...) ctx.preview && preview_info() do_update_registry && update_registry(ctx) diff --git a/stdlib/Pkg/src/REPLMode.jl b/stdlib/Pkg/src/REPLMode.jl index a7df17f52b8a04..c38749b968b142 100644 --- a/stdlib/Pkg/src/REPLMode.jl +++ b/stdlib/Pkg/src/REPLMode.jl @@ -1,5 +1,3 @@ -# This file is a part of Julia. License is MIT: https://julialang.org/license - module REPLMode using Markdown @@ -11,45 +9,6 @@ import REPL: LineEdit, REPLCompletions import ..devdir, ..Types.casesensitive_isdir, ..TOML using ..Types, ..Display, ..Operations, ..API -############ -# Commands # -############ -@enum(CommandKind, CMD_HELP, CMD_STATUS, CMD_SEARCH, CMD_ADD, CMD_RM, CMD_UP, - CMD_TEST, CMD_GC, CMD_PREVIEW, CMD_INIT, CMD_BUILD, CMD_FREE, - CMD_PIN, CMD_CHECKOUT, CMD_DEVELOP, CMD_GENERATE, CMD_PRECOMPILE, - CMD_INSTANTIATE, CMD_RESOLVE, CMD_ACTIVATE, CMD_DEACTIVATE) - -struct Command - kind::CommandKind - val::String -end -Base.show(io::IO, cmd::Command) = print(io, cmd.val) - -const cmds = Dict( - "help" => CMD_HELP, - "?" => CMD_HELP, - "status" => CMD_STATUS, - "st" => CMD_STATUS, - "add" => CMD_ADD, - "rm" => CMD_RM, - "remove" => CMD_RM, - "up" => CMD_UP, - "update" => CMD_UP, - "test" => CMD_TEST, - "gc" => CMD_GC, - "preview" => CMD_PREVIEW, - "build" => CMD_BUILD, - "pin" => CMD_PIN, - "free" => CMD_FREE, - "develop" => CMD_DEVELOP, - "dev" => CMD_DEVELOP, - "generate" => CMD_GENERATE, - "precompile" => CMD_PRECOMPILE, - "instantiate" => CMD_INSTANTIATE, - "resolve" => CMD_RESOLVE, - "activate" => CMD_ACTIVATE, -) - ################# # Git revisions # ################# @@ -60,64 +19,133 @@ end ########### # Options # ########### -@enum(OptionKind, OPT_ENV, OPT_PROJECT, OPT_MANIFEST, OPT_MAJOR, OPT_MINOR, - OPT_PATCH, OPT_FIXED, OPT_COVERAGE, OPT_NAME, - OPT_LOCAL, OPT_SHARED) - -function Types.PackageMode(opt::OptionKind) - opt == OPT_MANIFEST && return PKGMODE_MANIFEST - opt == OPT_PROJECT && return PKGMODE_PROJECT - throw(ArgumentError("invalid option $opt")) +#TODO should this opt be removed: ("name", :cmd, :temp => false) +struct OptionSpec + name::String + short_name::Union{Nothing,String} + api::Pair{Symbol, Any} + is_switch::Bool end -function Types.UpgradeLevel(opt::OptionKind) - opt == OPT_MAJOR && return UPLEVEL_MAJOR - opt == OPT_MINOR && return UPLEVEL_MINOR - opt == OPT_PATCH && return UPLEVEL_PATCH - opt == OPT_FIXED && return UPLEVEL_FIXED - throw(ArgumentError("invalid option $opt")) +@enum(OptionClass, OPT_ARG, OPT_SWITCH) +const OptionDeclaration = Tuple{Union{String,Vector{String}}, # name + short_name? + OptionClass, # arg or switch + Pair{Symbol, Any} # api keywords + } + +function OptionSpec(x::OptionDeclaration)::OptionSpec + get_names(name::String) = (name, nothing) + function get_names(names::Vector{String}) + @assert length(names) == 2 + return (names[1], names[2]) + end + + is_switch = x[2] == OPT_SWITCH + api = x[3] + (name, short_name) = get_names(x[1]) + #TODO assert matching lex regex + if !is_switch + @assert api.second === nothing || hasmethod(api.second, Tuple{String}) + end + return OptionSpec(name, short_name, api, is_switch) end -struct Option - kind::OptionKind - val::String - argument::Union{String, Nothing} - Option(kind::OptionKind, val::String) = new(kind, val, nothing) - function Option(kind::OptionKind, val::String, argument::Union{String, Nothing}) - if kind in (OPT_PROJECT, OPT_MANIFEST, OPT_MAJOR, - OPT_MINOR, OPT_PATCH, OPT_FIXED) && - argument !== nothing - cmderror("the `$val` option does not take an argument") - elseif kind in (OPT_ENV,) && argument == nothing - cmderror("the `$val` option requires an argument") +function OptionSpecs(decs::Vector{OptionDeclaration})::Dict{String, OptionSpec} + specs = Dict() + for x in decs + opt_spec = OptionSpec(x) + @assert get(specs, opt_spec.name, nothing) === nothing # don't overwrite + specs[opt_spec.name] = opt_spec + if opt_spec.short_name !== nothing + @assert get(specs, opt_spec.short_name, nothing) === nothing # don't overwrite + specs[opt_spec.short_name] = opt_spec end - new(kind, val, argument) end + return specs end -Base.show(io::IO, opt::Option) = print(io, "--$(opt.val)", opt.argument == nothing ? "" : "=$(opt.argument)") -const opts = Dict( - "env" => OPT_ENV, - "project" => OPT_PROJECT, - "p" => OPT_PROJECT, - "manifest" => OPT_MANIFEST, - "m" => OPT_MANIFEST, - "major" => OPT_MAJOR, - "minor" => OPT_MINOR, - "patch" => OPT_PATCH, - "fixed" => OPT_FIXED, - "coverage" => OPT_COVERAGE, - "name" => OPT_NAME, - "local" => OPT_LOCAL, - "shared" => OPT_SHARED, -) +struct Option + val::String + argument::Union{Nothing,String} + Option(val::AbstractString) = new(val, nothing) + Option(val::AbstractString, arg::Union{Nothing,String}) = new(val, arg) +end +Base.show(io::IO, opt::Option) = print(io, "--$(opt.val)", opt.argument == nothing ? "" : "=$(opt.argument)") function parse_option(word::AbstractString)::Option m = match(r"^(?: -([a-z]) | --([a-z]{2,})(?:\s*=\s*(\S*))? )$"ix, word) - m == nothing && cmderror("invalid option: ", repr(word)) - k = m.captures[1] != nothing ? m.captures[1] : m.captures[2] - haskey(opts, k) || cmderror("invalid option: ", repr(word)) - return Option(opts[k], String(k), m.captures[3] == nothing ? nothing : String(m.captures[3])) + m == nothing && cmderror("malformed option: ", repr(word)) + option_name = (m.captures[1] != nothing ? m.captures[1] : m.captures[2]) + option_arg = (m.captures[3] == nothing ? nothing : String(m.captures[3])) + return Option(option_name, option_arg) +end + +meta_option_declarations = OptionDeclaration[ + ("env", OPT_ARG, :env => arg->EnvCache(Base.parse_env(arg))) +] +meta_option_specs = OptionSpecs(meta_option_declarations) + +################ +# Command Spec # +################ +@enum(CommandKind, CMD_HELP, CMD_RM, CMD_ADD, CMD_DEVELOP, CMD_UP, + CMD_STATUS, CMD_TEST, CMD_GC, CMD_BUILD, CMD_PIN, + CMD_FREE, CMD_GENERATE, CMD_RESOLVE, CMD_PRECOMPILE, + CMD_INSTANTIATE, CMD_ACTIVATE, CMD_PREVIEW, + CMD_REGISTRY_ADD, + ) +@enum(ArgClass, ARG_RAW, ARG_PKG, ARG_VERSION, ARG_REV, ARG_ALL) +struct ArgSpec + class::ArgClass + count::Vector{Int} +end +const CommandDeclaration = Tuple{CommandKind, + Vector{String}, # names + Function, # handler + Tuple{ArgClass, Vector{Int}}, # argument count + Vector{OptionDeclaration}, # options + Union{Nothing, Markdown.MD}, #help + } +struct CommandSpec + kind::CommandKind + names::Vector{String} + handler::Function + argument_spec::ArgSpec # note: just use range operator for max/min + option_specs::Dict{String, OptionSpec} + help::Union{Nothing, Markdown.MD} +end +command_specs = Dict{String,CommandSpec}() # TODO remove this ? + +function SuperSpecs(foo)::Dict{String,Dict{String,CommandSpec}} + super_specs = Dict() + for x in foo + sub_specs = CommandSpecs(x.second) + for name in x.first + @assert get(super_specs, name, nothing) === nothing + super_specs[name] = sub_specs + end + end + return super_specs +end + +# populate a dictionary: command_name -> command_spec +function CommandSpecs(declarations::Vector{CommandDeclaration})::Dict{String,CommandSpec} + specs = Dict() + for dec in declarations + names = dec[2] + spec = CommandSpec(dec[1], + names, + dec[3], + ArgSpec(dec[4]...), + OptionSpecs(dec[5]), + dec[end]) + for name in names + # TODO regex check name + @assert get(specs, name, nothing) === nothing + specs[name] = spec + end + end + return specs end ################### @@ -154,44 +182,97 @@ end ################ # REPL parsing # ################ -const lex_re = r"^[\?\./\+\-](?!\-) | ((git|ssh|http(s)?)|(git@[\w\-\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)? | [^@\#\s;]+\s*=\s*[^@\#\s;]+ | \#\s*[^@\#\s;]* | @\s*[^@\#\s;]* | [^@\#\s;]+|;"x +mutable struct Statement + command::Union{Nothing,CommandSpec} + options::Vector{String} + arguments::Vector{String} + meta_options::Vector{String} + Statement() = new(nothing, [], [], []) +end -const Token = Union{Command, Option, VersionRange, String, Rev} - -function tokenize(cmd::String)::Vector{Vector{Token}} - # phase 1: tokenize accoring to whitespace / quotes - chunks = parse_quotes(cmd) - # phase 2: tokenzie unquoted tokens according to pkg REPL syntax - words::Vector{String} = [] - for chunk in chunks - is_quoted = chunk[1] - word = chunk[2] - if is_quoted - push!(words, word) - else # break unquoted chunks further according to lexer - # note: space before `$word` is necessary to keep using current `lex_re` - # v - append!(words, map(m->m.match, eachmatch(lex_re, " $word"))) - end +struct QuotedWord + word::String + isquoted::Bool +end + +function parse(cmd::String)::Vector{Statement} + # replace new lines with ; to support multiline commands + cmd = replace(replace(cmd, "\r\n" => "; "), "\n" => "; ") + # tokenize accoring to whitespace / quotes + qwords = parse_quotes(cmd) + # tokenzie unquoted tokens according to pkg REPL syntax + words::Vector{String} = collect(Iterators.flatten(map(qword2word, qwords))) + # break up words according to ";"(doing this early makes subsequent processing easier) + word_groups = group_words(words) + # create statements + statements = map(Statement, word_groups) + return statements +end + +# vector of words -> structured statement +# minimal checking is done in this phase +function Statement(words) + is_option(word) = first(word) == '-' + statement = Statement() + + word = popfirst!(words) + # meta options + while is_option(word) + push!(statement.meta_options, word) + isempty(words) && cmderror("no command specified") + word = popfirst!(words) end + # command + if word in keys(super_specs) + super = super_specs[word] + word = popfirst!(words) + else + super = super_specs["package"] + end + command = get(super, word, nothing) + command !== nothing || cmderror("expected command. instead got [$word]") + statement.command = command + # command arguments + for word in words + push!((is_option(word) ? statement.options : statement.arguments), word) + end + return statement +end - commands = Vector{Token}[] - while !isempty(words) - push!(commands, tokenize!(words)) +# break up words according to `;`(doing this early makes subsequent processing easier) +# the final group does not require a trailing `;` +function group_words(words)::Vector{Vector{String}} + statements = Vector{String}[] + x = String[] + for word in words + if word == ";" + isempty(x) ? cmderror("empty statement") : push!(statements, x) + x = String[] + else + push!(x, word) + end end - return commands + isempty(x) || push!(statements, x) + return statements +end + +const lex_re = r"^[\?\./\+\-](?!\-) | ((git|ssh|http(s)?)|(git@[\w\-\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)? | [^@\#\s;]+\s*=\s*[^@\#\s;]+ | \#\s*[^@\#\s;]* | @\s*[^@\#\s;]* | [^@\#\s;]+|;"x + +function qword2word(qword::QuotedWord) + return qword.isquoted ? [qword.word] : map(m->m.match, eachmatch(lex_re, " $(qword.word)")) + # ^ + # note: space before `$word` is necessary to keep using current `lex_re` end -function parse_quotes(cmd::String) +function parse_quotes(cmd::String)::Vector{QuotedWord} in_doublequote = false in_singlequote = false - all_tokens::Array = [] - token_in_progress::Array{Char} = [] + qwords = QuotedWord[] + token_in_progress = Char[] push_token!(is_quoted) = begin - complete_token = String(token_in_progress) + push!(qwords, QuotedWord(String(token_in_progress), is_quoted)) empty!(token_in_progress) - push!(all_tokens, (is_quoted, complete_token)) end for c in cmd @@ -216,644 +297,364 @@ function parse_quotes(cmd::String) end end if (in_doublequote || in_singlequote) - ArgumentError("unterminated quote") + cmderror("unterminated quote") else push_token!(false) end # to avoid complexity in the main loop, empty tokens are allowed above and # filtered out before returning - isnotempty(x) = !isempty(x[2]) - filter!(isnotempty, all_tokens) - return all_tokens + return filter(x->!isempty(x.word), qwords) end -function tokenize!(words::Vector{<:AbstractString})::Vector{Token} - tokens = Token[] - help_mode = false - preview_mode = false - # First parse a Command or a modifier (help / preview) + Command - while !isempty(words) - word = popfirst!(words) - if word[1] == '-' && length(word) > 1 - push!(tokens, parse_option(word)) - else - haskey(cmds, word) || cmderror("invalid command: ", repr(word)) - cmdkind = cmds[word] - push!(tokens, Command(cmdkind, word)) - # If help / preview and not in help mode we want to eat another cmd - if !help_mode - cmdkind == CMD_HELP && (help_mode = true; continue) - cmdkind == CMD_PREVIEW && (preview_mode = true; continue) - end - break - end - end - if isempty(tokens) || !(tokens[end] isa Command) - cmderror("no package command given") - end - # Now parse the arguments / options to the command - while !isempty(words) - word = popfirst!(words) - if word == ";" - return tokens - elseif first(word) == '-' - push!(tokens, parse_option(word)) - elseif first(word) == '@' - push!(tokens, VersionRange(strip(word[2:end]))) - elseif first(word) == '#' - push!(tokens, Rev(word[2:end])) - else - push!(tokens, String(word)) - end - end - return tokens +############## +# PkgCommand # +############## +const Token = Union{String, VersionRange, Rev} +const PkgArguments = Union{Vector{String}, Vector{PackageSpec}} +#TODO embed spec in PkgCommand? +struct PkgCommand + meta_options::Vector{Option} + spec::CommandSpec + options::Vector{Option} + arguments::PkgArguments + PkgCommand() = new([], "", [], []) + PkgCommand(meta_opts, cmd_name, opts, args) = new(meta_opts, cmd_name, opts, args) end +const APIOption = Pair{Symbol, Any} +APIOptions(command::PkgCommand)::Vector{APIOption} = + APIOptions(command.options, command.spec.option_specs) + +function APIOptions(options::Vector{Option}, + specs::Dict{String, OptionSpec}, + )::Vector{APIOption} + return map(options) do opt + spec = specs[opt.val] + # opt is switch + spec.is_switch && return spec.api + # no opt wrapper -> just use raw argument + spec.api.second === nothing && return spec.api.first => opt.argument + # given opt wrapper + return spec.api.first => spec.api.second(opt.argument) + end +end -############# -# Execution # -############# - -function do_cmd(repl::REPL.AbstractREPL, input::String; do_rethrow=false) - try - commands = tokenize(input) - for command in commands - do_cmd!(command, repl) - end - catch err - if do_rethrow - rethrow(err) - end - if err isa CommandError || err isa ResolverError - Base.display_error(repl.t.err_stream, ErrorException(sprint(showerror, err)), Ptr{Nothing}[]) - else - Base.display_error(repl.t.err_stream, err, Base.catch_backtrace()) - end +function key_api(key::Symbol, api_opts::Vector{APIOption}) + index = findfirst(x->x.first == key, api_opts) + if index !== nothing + return api_opts[index].second end end -function enforce_argument_order(tokens::Vector{Token}) - prev_token = nothing - function check_prev_token(valid_type::DataType, error_message::AbstractString) - prev_token isa valid_type || cmderror(error_message) +set_default!(opt, api_opts::Vector{APIOption}) = + key_api(opt.first, api_opts) === nothing && push!(api_opts, opt) + +function enforce_argument_order(args::Vector{Token}) + prev_arg = nothing + function check_prev_arg(valid_type::DataType, error_message::AbstractString) + prev_arg isa valid_type || cmderror(error_message) end - for token in tokens - if token isa VersionRange - check_prev_token(String, "package name/uuid must precede version spec `@$token`") - elseif token isa Rev - check_prev_token(String, "package name/uuid must precede rev spec `#$(token.rev)`") + for arg in args + if arg isa VersionRange + check_prev_arg(String, "package name/uuid must precede version spec `@$arg`") + elseif arg isa Rev + check_prev_arg(String, "package name/uuid must precede rev spec `#$(arg.rev)`") end - prev_token = token + prev_arg = arg end end -function do_cmd!(tokens::Vector{Token}, repl) - cmd = env_opt = nothing - while !isempty(tokens) - token = popfirst!(tokens) - if token isa Command - cmd = token - break - elseif token isa Option - # Only OPT_ENV is allowed before a command - if token.kind == OPT_ENV - env_opt = Base.parse_env(token.argument) - else - cmderror("unrecognized command option: `$token`") - end - else - cmderror("misplaced token: ", token) - end +function word2token(word::AbstractString)::Token + if first(word) == '@' + return VersionRange(word[2:end]) + elseif first(word) == '#' + return Rev(word[2:end]) + else + return String(word) end +end - if cmd.kind == CMD_ACTIVATE - return Base.invokelatest(do_activate!, Base.active_project() === nothing ? - nothing : EnvCache(env_opt), tokens) +function enforce_arg_spec(raw_args::Vector{String}, class::ArgClass) + # TODO is there a more idiomatic way to do this? + function has_types(arguments::Vector{Token}, types::Vector{DataType}) + return !isempty(filter(x->typeof(x) in types, arguments)) end - ctx = Context(env = EnvCache(env_opt)) - if cmd.kind == CMD_PREVIEW - ctx.preview = true - isempty(tokens) && cmderror("expected a command to preview") - cmd = popfirst!(tokens) - end + class == ARG_RAW && return raw_args + args::Vector{Token} = map(word2token, raw_args) + class == ARG_ALL && return args - enforce_argument_order(tokens) - - # Using invokelatest to hide the functions from inference. - # Otherwise it would try to infer everything here. - cmd.kind == CMD_INIT ? Base.invokelatest( do_init!, ctx, tokens) : - cmd.kind == CMD_HELP ? Base.invokelatest( do_help!, ctx, tokens, repl) : - cmd.kind == CMD_RM ? Base.invokelatest( do_rm!, ctx, tokens) : - cmd.kind == CMD_ADD ? Base.invokelatest(do_add_or_develop!, ctx, tokens, CMD_ADD) : - cmd.kind == CMD_CHECKOUT ? Base.invokelatest(do_add_or_develop!, ctx, tokens, CMD_DEVELOP) : - cmd.kind == CMD_DEVELOP ? Base.invokelatest(do_add_or_develop!, ctx, tokens, CMD_DEVELOP) : - cmd.kind == CMD_UP ? Base.invokelatest( do_up!, ctx, tokens) : - cmd.kind == CMD_STATUS ? Base.invokelatest( do_status!, ctx, tokens) : - cmd.kind == CMD_TEST ? Base.invokelatest( do_test!, ctx, tokens) : - cmd.kind == CMD_GC ? Base.invokelatest( do_gc!, ctx, tokens) : - cmd.kind == CMD_BUILD ? Base.invokelatest( do_build!, ctx, tokens) : - cmd.kind == CMD_PIN ? Base.invokelatest( do_pin!, ctx, tokens) : - cmd.kind == CMD_FREE ? Base.invokelatest( do_free!, ctx, tokens) : - cmd.kind == CMD_GENERATE ? Base.invokelatest( do_generate!, ctx, tokens) : - cmd.kind == CMD_RESOLVE ? Base.invokelatest( do_resolve!, ctx, tokens) : - cmd.kind == CMD_PRECOMPILE ? Base.invokelatest( do_precompile!, ctx, tokens) : - cmd.kind == CMD_INSTANTIATE ? Base.invokelatest( do_instantiate!, ctx, tokens) : - cmderror("`$cmd` command not yet implemented") - return + if class == ARG_PKG && has_types(args, [VersionRange, Rev]) + cmderror("no versioned packages allowed") + elseif class == ARG_REV && has_types(args, [VersionRange]) + cmderror("no versioned packages allowed") + elseif class == ARG_VERSION && has_types(args, [Rev]) + cmderror("no reved packages allowed") + end + return args end -const help = md""" - -**Welcome to the Pkg REPL-mode**. To return to the `julia>` prompt, either press -backspace when the input line is empty or press Ctrl+C. - - -**Synopsis** - - pkg> [--env=...] cmd [opts] [args] - -Multiple commands can be given on the same line by interleaving a `;` between the commands. - -**Environment** - -The `--env` meta option determines which project environment to manipulate. By -default, this looks for a git repo in the parents directories of the current -working directory, and if it finds one, it uses that as an environment. Otherwise, -it uses a named environment (typically found in `~/.julia/environments`) looking -for environments named `v$(VERSION.major).$(VERSION.minor).$(VERSION.patch)`, -`v$(VERSION.major).$(VERSION.minor)`, `v$(VERSION.major)` or `default` in order. - -**Commands** - -What action you want the package manager to take: - -`help`: show this message - -`status`: summarize contents of and changes to environment - -`add`: add packages to project - -`develop`: clone the full package repo locally for development - -`rm`: remove packages from project or manifest - -`up`: update packages in manifest - -`test`: run tests for packages - -`build`: run the build script for packages - -`pin`: pins the version of packages - -`free`: undoes a `pin`, `develop`, or stops tracking a repo. - -`instantiate`: downloads all the dependencies for the project - -`resolve`: resolves to update the manifest from changes in dependencies of -developed packages - -`generate`: generate files for a new project - -`preview`: previews a subsequent command without affecting the current state - -`precompile`: precompile all the project dependencies - -`gc`: garbage collect packages not used for a significant time - -`activate`: set the primary environment the package manager manipulates -""" - -const helps = Dict( - CMD_HELP => md""" - - help - - Display this message. - - help cmd ... - - Display usage information for commands listed. - - Available commands: `help`, `status`, `add`, `rm`, `up`, `preview`, `gc`, `test`, `build`, `free`, `pin`, `develop`. - """, CMD_STATUS => md""" - - status - status [-p|--project] - status [-m|--manifest] - - Show the status of the current environment. By default, the full contents of - the project file is summarized, showing what version each package is on and - how it has changed since the last git commit (if in a git repo), as well as - any changes to manifest packages not already listed. In `--project` mode, the - status of the project file is summarized. In `--manifest` mode the output also - includes the dependencies of explicitly added packages. - """, CMD_GENERATE => md""" - - generate pkgname - - Create a project called `pkgname` in the current folder. - """, - CMD_ADD => md""" - - add pkg[=uuid] [@version] [#rev] ... - - Add package `pkg` to the current project file. If `pkg` could refer to - multiple different packages, specifying `uuid` allows you to disambiguate. - `@version` optionally allows specifying which versions of packages. Versions - may be specified by `@1`, `@1.2`, `@1.2.3`, allowing any version with a prefix - that matches, or ranges thereof, such as `@1.2-3.4.5`. A git-revision can be - specified by `#branch` or `#commit`. - - If a local path is used as an argument to `add`, the path needs to be a git repository. - The project will then track that git repository just like if it is was tracking a remote repository online. - - **Examples** - ``` - pkg> add Example - pkg> add Example@0.5 - pkg> add Example#master - pkg> add Example#c37b675 - pkg> add https://github.com/JuliaLang/Example.jl#master - pkg> add git@github.com:JuliaLang/Example.jl.git - pkg> add Example=7876af07-990d-54b4-ab0e-23690620f79a - ``` - """, CMD_RM => md""" - - rm [-p|--project] pkg[=uuid] ... - - Remove package `pkg` from the project file. Since the name `pkg` can only - refer to one package in a project this is unambiguous, but you can specify - a `uuid` anyway, and the command is ignored, with a warning if package name - and UUID do not mactch. When a package is removed from the project file, it - may still remain in the manifest if it is required by some other package in - the project. Project mode operation is the default, so passing `-p` or - `--project` is optional unless it is preceded by the `-m` or `--manifest` - options at some earlier point. - - rm [-m|--manifest] pkg[=uuid] ... - - Remove package `pkg` from the manifest file. If the name `pkg` refers to - multiple packages in the manifest, `uuid` disambiguates it. Removing a package - from the manifest forces the removal of all packages that depend on it, as well - as any no-longer-necessary manifest packages due to project package removals. - """, CMD_UP => md""" - - up [-p|project] [opts] pkg[=uuid] [@version] ... - up [-m|manifest] [opts] pkg[=uuid] [@version] ... - - opts: --major | --minor | --patch | --fixed - - Update the indicated package within the constraints of the indicated version - specifications. Versions may be specified by `@1`, `@1.2`, `@1.2.3`, allowing - any version with a prefix that matches, or ranges thereof, such as `@1.2-3.4.5`. - In `--project` mode, package specifications only match project packages, while - in `manifest` mode they match any manifest package. Bound level options force - the following packages to be upgraded only within the current major, minor, - patch version; if the `--fixed` upgrade level is given, then the following - packages will not be upgraded at all. - """, CMD_PREVIEW => md""" - - preview cmd - - Runs the command `cmd` in preview mode. This is defined such that no side effects - will take place i.e. no packages are downloaded and neither the project nor manifest - is modified. - """, CMD_TEST => md""" - - test [opts] pkg[=uuid] ... - - opts: --coverage - - Run the tests for package `pkg`. This is done by running the file `test/runtests.jl` - in the package directory. The option `--coverage` can be used to run the tests with - coverage enabled. - """, CMD_GC => md""" - - Deletes packages that cannot be reached from any existing environment. - """, CMD_BUILD =>md""" - - build pkg[=uuid] ... - - Run the build script in deps/build.jl for each package in `pkg`` and all of their dependencies in depth-first recursive order. - If no packages are given, runs the build scripts for all packages in the manifest. - """, CMD_PIN => md""" - - pin pkg[=uuid] ... - - Pin packages to given versions, or the current version if no version is specified. A pinned package has its version fixed and will not be upgraded or downgraded. - A pinned package has the symbol `⚲` next to its version in the status list. - """, CMD_FREE => md""" - free pkg[=uuid] ... - - Free a pinned package `pkg`, which allows it to be upgraded or downgraded again. If the package is checked out (see `help develop`) then this command - makes the package no longer being checked out. - """, CMD_DEVELOP => md""" - develop [--shared|--local] pkg[=uuid] [#rev] ... - - Make a package available for development. If `pkg` is an existing local path that path will be recorded in - the manifest and used. Otherwise, a full git clone of `pkg` at rev `rev` is made. The location of the clone is - controlled by the `--shared` (default) and `--local` arguments. The `--shared` location defaults to - `~/.julia/dev`, but can be controlled with the `JULIA_PKG_DEVDIR` environment variable. When `--local` is given, - the clone is placed in a `dev` folder in the current project. - This operation is undone by `free`. - - *Example* - ```jl - pkg> develop Example - pkg> develop Example#master - pkg> develop Example#c37b675 - pkg> develop https://github.com/JuliaLang/Example.jl#master - pkg> develop --local Example - ``` - """, CMD_PRECOMPILE => md""" - precompile - - Precompile all the dependencies of the project by running `import` on all of them in a new process. - """, CMD_INSTANTIATE => md""" - instantiate - instantiate [-m|--manifest] - instantiate [-p|--project] - - Download all the dependencies for the current project at the version given by the project's manifest. - If no manifest exists or the `--project` option is given, resolve and download the dependencies compatible with the project. - """, CMD_RESOLVE => md""" - resolve - - Resolve the project i.e. run package resolution and update the Manifest. This is useful in case the dependencies of developed - packages have changed causing the current Manifest to_indices be out of sync. - """ -) - -function do_help!( - ctk::Context, - tokens::Vector{Token}, - repl::REPL.AbstractREPL, -) - disp = REPL.REPLDisplay(repl) - if isempty(tokens) - Base.display(disp, help) - return - end - help_md = md"" - for token in tokens - if token isa Command - if haskey(helps, token.kind) - isempty(help_md.content) || - push!(help_md.content, md"---") - push!(help_md.content, helps[token.kind].content) +function package_args(args::Vector{Token}, spec::CommandSpec)::Vector{PackageSpec} + pkgs = PackageSpec[] + for arg in args + if arg isa String + is_add_or_develop = spec.kind in (CMD_ADD, CMD_DEVELOP) + push!(pkgs, parse_package(arg; add_or_develop=is_add_or_develop)) + elseif arg isa VersionRange + pkgs[end].version = arg + elseif arg isa Rev + pkg = pkgs[end] + if pkg.repo == nothing + pkg.repo = Types.GitRepo("", arg.rev) else - cmderror("Sorry, I don't have any help for the `$(token.val)` command.") + pkgs[end].repo.rev = arg.rev end else - error("invalid usage of help command") + assert(false) end end - Base.display(disp, help_md) + return pkgs end -function do_rm!(ctx::Context, tokens::Vector{Token}) - # tokens: package names and/or uuids - mode = PKGMODE_PROJECT - pkgs = PackageSpec[] - while !isempty(tokens) - token = popfirst!(tokens) - if token isa String - push!(pkgs, parse_package(token)) - pkgs[end].mode = mode - elseif token isa VersionRange - cmderror("`rm` does not take version specs") - elseif token isa Option - if token.kind in (OPT_PROJECT, OPT_MANIFEST) - mode = PackageMode(token.kind) - else - cmderror("invalid option for `rm`: $token") - end - end - end - isempty(pkgs) && - cmderror("`rm` – list packages to remove") - API.rm(ctx, pkgs) +function enforce_arg_count(count::Vector{Int}, args::PkgArguments) + isempty(count) && return + length(args) in count || + cmderror("Wrong number of arguments") end -function do_add_or_develop!(ctx::Context, tokens::Vector{Token}, cmd::CommandKind) - @assert cmd in (CMD_ADD, CMD_DEVELOP) - mode = cmd == CMD_ADD ? :add : :develop - # tokens: package names and/or uuids, optionally followed by version specs - isempty(tokens) && - cmderror("`$mode` – list packages to $mode") - pkgs = PackageSpec[] - dev_mode = OPT_SHARED # TODO: Make this default configurable - while !isempty(tokens) - token = popfirst!(tokens) - if token isa String - push!(pkgs, parse_package(token; add_or_develop=true)) - elseif token isa VersionRange - pkgs[end].version = VersionSpec(token) - elseif token isa Rev - # WE did not get the repo from the - pkg = pkgs[end] - if pkg.repo == nothing - pkg.repo = Types.GitRepo("", token.rev) - else - pkgs[end].repo.rev = token.rev - end - elseif token isa Option - if mode === :develop && token.kind in (OPT_LOCAL, OPT_SHARED) - dev_mode = token.kind - else - cmderror("`$mode` doesn't take options: $token") - end - end +function enforce_args(raw_args::Vector{String}, spec::ArgSpec, cmd_spec::CommandSpec)::PkgArguments + if spec.class == ARG_RAW + enforce_arg_count(spec.count, raw_args) + return raw_args end - dev_dir = mode === :add ? nothing : dev_mode == OPT_LOCAL ? - joinpath(dirname(ctx.env.project_file), "dev") : nothing - return API.add_or_develop(ctx, pkgs, mode=mode, devdir=dev_dir) + + args = enforce_arg_spec(raw_args, spec.class) + enforce_argument_order(args) + pkgs = package_args(args, cmd_spec) + enforce_arg_count(spec.count, pkgs) + return pkgs end -function do_up!(ctx::Context, tokens::Vector{Token}) - # tokens: - # - upgrade levels as options: --[fixed|patch|minor|major] - # - package names and/or uuids, optionally followed by version specs - pkgs = PackageSpec[] - mode = PKGMODE_PROJECT - level = UPLEVEL_MAJOR - while !isempty(tokens) - token = popfirst!(tokens) - if token isa String - push!(pkgs, parse_package(token)) - pkgs[end].version = level - pkgs[end].mode = mode - elseif token isa VersionRange - pkgs[end].version = VersionSpec(token) - elseif token isa Option - if token.kind in (OPT_PROJECT, OPT_MANIFEST) - mode = PackageMode(token.kind) - elseif token.kind in (OPT_MAJOR, OPT_MINOR, OPT_PATCH, OPT_FIXED) - level = UpgradeLevel(token.kind) - else - cmderror("invalid option for `up`: $(token)") - end - end +function enforce_option(option::String, specs::Dict{String,OptionSpec})::Option + opt = parse_option(option) + spec = get(specs, opt.val, nothing) + spec !== nothing || + cmderror("option '$(opt.val)' is not a valid option") + if spec.is_switch + opt.argument === nothing || + cmderror("option '$(opt.val)' does not take an argument, but '$(opt.argument)' given") + else # option takes an argument + opt.argument !== nothing || + cmderror("option '$(opt.val)' expects an argument, but no argument given") end - API.up(ctx, pkgs; level=level, mode=mode) + return opt end -function do_pin!(ctx::Context, tokens::Vector{Token}) - pkgs = PackageSpec[] - while !isempty(tokens) - token = popfirst!(tokens) - if token isa String - push!(pkgs, parse_package(token)) - elseif token isa VersionRange - if token.lower != token.upper - cmderror("pinning a package requires a single version, not a versionrange") - end - pkgs[end].version = VersionSpec(token) - else - cmderror("free only takes a list of packages ") - end +function enforce_meta_options(options::Vector{String}, specs::Dict{String,OptionSpec})::Vector{Option} + meta_opt_names = keys(specs) + return map(options) do opt + tok = enforce_option(opt, specs) + tok.val in meta_opt_names || + cmderror("option '$opt' is not a valid meta option.") + #TODO hint that maybe they intended to use it as a command option + return tok end - API.pin(ctx, pkgs) end -function do_free!(ctx::Context, tokens::Vector{Token}) - pkgs = PackageSpec[] - while !isempty(tokens) - token = popfirst!(tokens) - if token isa String - push!(pkgs, parse_package(token)) +function enforce_opts(options::Vector{String}, specs::Dict{String,OptionSpec})::Vector{Option} + unique_keys = Symbol[] + get_key(opt::Option) = specs[opt.val].api.first + + # final parsing + toks = map(x->enforce_option(x,specs),options) + # checking + for opt in toks + # valid option + opt.val in keys(specs) || + cmderror("option '$(opt.val)' is not supported") + # conflicting options + key = get_key(opt) + if key in unique_keys + conflicting = filter(opt->get_key(opt) == key, toks) + cmderror("Conflicting options: $conflicting") else - cmderror("free only takes a list of packages") + push!(unique_keys, key) end end - API.free(ctx, pkgs) + return toks end -function do_status!(ctx::Context, tokens::Vector{Token}) - mode = PKGMODE_COMBINED - while !isempty(tokens) - token = popfirst!(tokens) - if token isa Option - if token.kind in (OPT_PROJECT, OPT_MANIFEST) - mode = PackageMode(token.kind) - else - cmderror("invalid option for `status`: $(token)") - end - else - cmderror("`status` does not take arguments") - end - end - Display.status(ctx, mode) +# this the entry point for the majority of input checks +function PkgCommand(statement::Statement)::PkgCommand + meta_opts = enforce_meta_options(statement.meta_options, + meta_option_specs) + args = enforce_args(statement.arguments, + statement.command.argument_spec, + statement.command) + opts = enforce_opts(statement.options, statement.command.option_specs) + return PkgCommand(meta_opts, statement.command, opts, args) end -# TODO , test recursive dependencies as on option. -function do_test!(ctx::Context, tokens::Vector{Token}) - pkgs = PackageSpec[] - coverage = false - while !isempty(tokens) - token = popfirst!(tokens) - if token isa String - pkg = parse_package(token) - pkg.mode = PKGMODE_MANIFEST - push!(pkgs, pkg) - elseif token isa Option - if token.kind == OPT_COVERAGE - coverage = true - else - cmderror("invalid option for `test`: $token") - end +############# +# Execution # +############# +function do_cmd(repl::REPL.AbstractREPL, input::String; do_rethrow=false) + try + statements = parse(input) + commands = map(PkgCommand, statements) + for cmd in commands + do_cmd!(cmd, repl) + end + catch err + if do_rethrow + rethrow(err) + end + if err isa CommandError || err isa ResolverError + Base.display_error(repl.t.err_stream, ErrorException(sprint(showerror, err)), Ptr{Nothing}[]) else - # TODO: Better error message - cmderror("invalid usage for `test`") + Base.display_error(repl.t.err_stream, err, Base.catch_backtrace()) end end - API.test(ctx, pkgs; coverage = coverage) end -function do_gc!(ctx::Context, tokens::Vector{Token}) - !isempty(tokens) && cmderror("`gc` does not take any arguments") - API.gc(ctx) +function do_cmd!(command::PkgCommand, repl) + meta_opts = APIOptions(command.meta_options, meta_option_specs) + ctx = Context(meta_opts...) + spec = command.spec + + # REPL specific commands + if spec.kind == CMD_HELP + return Base.invokelatest(do_help!, ctx, command, repl) + elseif spec.kind == CMD_PREVIEW + ctx.preview = true + cmd = command.arguments[1] + cmd_spec = get(command_specs, cmd, nothing) + cmd_spec === nothing && + cmderror("'$cmd' is not a valid command") + spec = cmd_spec + command = PkgCommand([], cmd, [], PackageSpec[]) + end + + # API commands + # TODO is invokelatest still needed? + Base.invokelatest(spec.handler, ctx, command.arguments, APIOptions(command)) end -function do_build!(ctx::Context, tokens::Vector{Token}) - pkgs = PackageSpec[] - while !isempty(tokens) - token = popfirst!(tokens) - if token isa String - push!(pkgs, parse_package(token)) - else - cmderror("`build` only takes a list of packages") - end +function do_help!(ctk::Context, command::PkgCommand, repl::REPL.AbstractREPL) + disp = REPL.REPLDisplay(repl) + if isempty(command.arguments) + Base.display(disp, help) + return end - API.build(ctx, pkgs) + help_md = md"" + for arg in command.arguments + spec = get(command_specs, arg, nothing) + spec === nothing && + cmderror("'$arg' does not name a command") + spec.help === nothing && + cmderror("Sorry, I don't have any help for the `$arg` command.") + isempty(help_md.content) || + push!(help_md.content, md"---") + push!(help_md.content, spec.help) + end + Base.display(disp, help_md) end -function do_generate!(ctx::Context, tokens::Vector{Token}) - isempty(tokens) && cmderror("`generate` requires a project name as an argument") - token = popfirst!(tokens) - token isa String || cmderror("`generate` takes a name of the project to create") - isempty(tokens) || cmderror("`generate` takes a single project name as an argument") - API.generate(ctx, token) +# TODO set default Display.status keyword: mode = PKGMODE_COMBINED +function do_status!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) + set_default!(:mode => PKGMODE_COMBINED, api_opts) + Display.status(ctx, key_api(:mode, api_opts)) end -function do_precompile!(ctx::Context, tokens::Vector{Token}) - if !isempty(tokens) - cmderror("`precompile` does not take any arguments") - end - API.precompile(ctx) +# TODO remove the need to specify a handler function (not needed for REPL commands) +do_preview!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = nothing + +# TODO , test recursive dependencies as on option. +function do_test!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) + foreach(arg -> arg.mode = PKGMODE_MANIFEST, args) + API.test(ctx, args; api_opts...) end -function do_instantiate!(ctx::Context, tokens::Vector{Token}) - manifest = nothing - for token in tokens - if token isa Option - if token.kind == OPT_MANIFEST - manifest = true - elseif token.kind == OPT_PROJECT - manifest = false - else - cmderror("invalid option for `instantiate`: $(token)") - end - else - cmderror("invalid argument for `instantiate` :$(token)") - end +function do_registry_add!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) + println("This is a dummy function for now") + println("My args are:") + for arg in args + println("- $arg") end - API.instantiate(ctx; manifest=manifest) end -function do_resolve!(ctx::Context, tokens::Vector{Token}) - !isempty(tokens) && cmderror("`resolve` does not take any arguments") +do_precompile!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = + API.precompile(ctx) + +do_resolve!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = API.resolve(ctx) -end -function do_activate!(env::Union{EnvCache,Nothing}, tokens::Vector{Token}) - if isempty(tokens) +do_gc!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = + API.gc(ctx; api_opts...) + +do_instantiate!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = + API.instantiate(ctx; api_opts...) + +do_generate!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = + API.generate(ctx, args[1]) + +do_build!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = + API.build(ctx, args; api_opts...) + +do_rm!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = + API.rm(ctx, args; api_opts...) + +do_free!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = + API.free(ctx, args; api_opts...) + +do_up!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) = + API.up(ctx, args; api_opts...) + +function do_activate!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) + if isempty(args) return API.activate() + end + + path = args[1] + env = Base.active_project() === nothing ? nothing : ctx.env + devpath = nothing + if env !== nothing && haskey(env.project["deps"], path) + uuid = UUID(env.project["deps"][path]) + info = manifest_info(env, uuid) + devpath = haskey(info, "path") ? joinpath(dirname(env.project_file), info["path"]) : nothing + end + # `pkg> activate path` does the following + # 1. if path exists, activate that + # 2. if path exists in deps, and the dep is deved, activate that path (`devpath`) above + # 3. activate the non-existing directory (e.g. as in `pkg> activate . for initing a new dev`) + if Types.isdir_windows_workaround(path) + API.activate(abspath(path)) + elseif devpath !== nothing + API.activate(abspath(devpath)) else - path = popfirst!(tokens) - if !isempty(tokens) || !(path isa String) - cmderror("`activate` takes an optional path to the env to activate") - end - devpath = nothing - if env !== nothing && haskey(env.project["deps"], path) - uuid = UUID(env.project["deps"][path]) - info = manifest_info(env, uuid) - devpath = haskey(info, "path") ? joinpath(dirname(env.project_file), info["path"]) : nothing - end - # `pkg> activate path` does the following - # 1. if path exists, activate that - # 2. if path exists in deps, and the dep is deved, activate that path (`devpath` above) - # 3. activate the non-existing directory (e.g. as in `pkg> activate .` for initing a new env) - if Types.isdir_windows_workaround(path) - API.activate(abspath(path)) - elseif devpath !== nothing - API.activate(abspath(devpath)) - else - API.activate(abspath(path)) + API.activate(abspath(path)) + end +end + +function do_pin!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) + for arg in args + # TODO not sure this is correct + if arg.version.ranges[1].lower != arg.version.ranges[1].upper + cmderror("pinning a package requires a single version, not a versionrange") end end + API.pin(ctx, args; api_opts...) +end + +function do_add!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) + push!(api_opts, :mode => :add) + API.add_or_develop(ctx, args; api_opts...) +end + +function do_develop!(ctx::Context, args::PkgArguments, api_opts::Vector{APIOption}) + push!(api_opts, :mode => :develop) + API.add_or_develop(ctx, args; api_opts...) end ###################### @@ -872,10 +673,11 @@ function MiniREPL() end REPL.REPLDisplay(repl::MiniREPL) = repl.display -__init__() = minirepl[] = MiniREPL() const minirepl = Ref{MiniREPL}() +#= __init__() = =# minirepl[] = MiniREPL() + macro pkg_str(str::String) :($(do_cmd)(minirepl[], $str; do_rethrow=true)) end @@ -883,9 +685,25 @@ end pkgstr(str::String) = do_cmd(minirepl[], str; do_rethrow=true) # handle completions -all_commands_sorted = sort!(collect(keys(cmds))) +all_commands_sorted = [] +long_commands = [] +all_options_sorted = [] +long_options = [] + +all_commands_sorted = sort(collect(String,keys(command_specs))) long_commands = filter(c -> length(c) > 2, all_commands_sorted) -all_options_sorted = [length(opt) > 1 ? "--$opt" : "-$opt" for opt in sort!(collect(keys(opts)))] +function all_options() + all_opts = [] + for command in values(command_specs) + for opt_spec in command.option_specs + push!(all_opts, opt_spec.name) + opt_spec.short_name !== nothing && push!(all_opts, opt_spec.short_name) + end + end + unique!(all_opts) + return all_opts +end +all_options_sorted = [length(opt) > 1 ? "--$opt" : "-$opt" for opt in sort!(all_options())] long_options = filter(c -> length(c) > 2, all_options_sorted) struct PkgCompletionProvider <: LineEdit.CompletionProvider end @@ -912,7 +730,7 @@ function complete_option(s, i1, i2) end function complete_package(s, i1, i2, lastcommand, project_opt) - if lastcommand in [CMD_STATUS, CMD_RM, CMD_UP, CMD_TEST, CMD_BUILD, CMD_FREE, CMD_PIN, CMD_CHECKOUT] + if lastcommand in [CMD_STATUS, CMD_RM, CMD_UP, CMD_TEST, CMD_BUILD, CMD_FREE, CMD_PIN] return complete_installed_package(s, i1, i2, project_opt) elseif lastcommand in [CMD_ADD, CMD_DEVELOP] return complete_remote_package(s, i1, i2) @@ -971,25 +789,17 @@ function completions(full, index) end # tokenize input, don't offer any completions for invalid commands - tokens = try - tokenize(join(pre_words[1:end-1], ' '))[end] + statement = try + parse(join(pre_words[1:end-1], ' '))[end] catch return String[], 0:-1, false end - tokens = reverse!(tokens) - - lastcommand = nothing + lastcommand = statement.command.kind project_opt = true - for t in tokens - if t isa Command - lastcommand = t.kind - break - end - end - for t in tokens - if t isa Option && t.kind in [OPT_PROJECT, OPT_MANIFEST] - project_opt = t.kind == OPT_PROJECT + for opt in statement.options + if opt in ["--manifest", "--project", "-m", "-p"] + project_opt = opt in ["--project", "-p"] break end end @@ -1011,7 +821,7 @@ prev_prefix = "" function promptf() global prev_project_timestamp, prev_prefix, prev_project_file project_file = try - project_file = Base.active_project() + Types.find_project_file() catch nothing end @@ -1121,4 +931,385 @@ function repl_init(repl) return end +######## +# SPEC # +######## +command_declarations = [ +["registry"] => CommandDeclaration[ +( + CMD_REGISTRY_ADD, + ["add"], + do_registry_add!, + (ARG_PKG, []), + [], + nothing, +), +], #registry + +["package"] => CommandDeclaration[ +( CMD_TEST, + ["test"], + do_test!, + (ARG_PKG, []), + [ + ("coverage", OPT_SWITCH, :coverage => true), + ], + md""" + + test [opts] pkg[=uuid] ... + + opts: --coverage + +Run the tests for package `pkg`. This is done by running the file `test/runtests.jl` +in the package directory. The option `--coverage` can be used to run the tests with +coverage enabled. The `startup.jl` file is disabled during testing unless +julia is started with `--startup-file=yes`. + """, +),( CMD_HELP, + ["help", "?"], + do_help!, + (ARG_RAW, []), + [], + md""" + + help + +Display this message. + + help cmd ... + +Display usage information for commands listed. + +Available commands: `help`, `status`, `add`, `rm`, `up`, `preview`, `gc`, `test`, `build`, `free`, `pin`, `develop`. + """, +),( CMD_INSTANTIATE, + ["instantiate"], + do_instantiate!, + (ARG_RAW, [0]), + [ + (["project", "p"], OPT_SWITCH, :manifest => false), + (["manifest", "m"], OPT_SWITCH, :manifest => true), + ], + md""" + instantiate + instantiate [-m|--manifest] + instantiate [-p|--project] + +Download all the dependencies for the current project at the version given by the project's manifest. +If no manifest exists or the `--project` option is given, resolve and download the dependencies compatible with the project. + """, +),( CMD_RM, + ["remove", "rm"], + do_rm!, + (ARG_PKG, []), + [ + (["project", "p"], OPT_SWITCH, :mode => PKGMODE_PROJECT), + (["manifest", "m"], OPT_SWITCH, :mode => PKGMODE_MANIFEST), + ], + md""" + + rm [-p|--project] pkg[=uuid] ... + +Remove package `pkg` from the project file. Since the name `pkg` can only +refer to one package in a project this is unambiguous, but you can specify +a `uuid` anyway, and the command is ignored, with a warning if package name +and UUID do not mactch. When a package is removed from the project file, it +may still remain in the manifest if it is required by some other package in +the project. Project mode operation is the default, so passing `-p` or +`--project` is optional unless it is preceded by the `-m` or `--manifest` +options at some earlier point. + + rm [-m|--manifest] pkg[=uuid] ... + +Remove package `pkg` from the manifest file. If the name `pkg` refers to +multiple packages in the manifest, `uuid` disambiguates it. Removing a package +from the manifest forces the removal of all packages that depend on it, as well +as any no-longer-necessary manifest packages due to project package removals. + """, +),( CMD_ADD, + ["add"], + do_add!, + (ARG_ALL, []), + [], + md""" + + add pkg[=uuid] [@version] [#rev] ... + +Add package `pkg` to the current project file. If `pkg` could refer to +multiple different packages, specifying `uuid` allows you to disambiguate. +`@version` optionally allows specifying which versions of packages. Versions +may be specified by `@1`, `@1.2`, `@1.2.3`, allowing any version with a prefix +that matches, or ranges thereof, such as `@1.2-3.4.5`. A git-revision can be +specified by `#branch` or `#commit`. + +If a local path is used as an argument to `add`, the path needs to be a git repository. +The project will then track that git repository just like if it is was tracking a remote repository online. + +**Examples** +``` +pkg> add Example +pkg> add Example@0.5 +pkg> add Example#master +pkg> add Example#c37b675 +pkg> add https://github.com/JuliaLang/Example.jl#master +pkg> add git@github.com:JuliaLang/Example.jl.git +pkg> add Example=7876af07-990d-54b4-ab0e-23690620f79a +``` + """, +),( CMD_DEVELOP, + ["develop", "dev"], + do_develop!, + (ARG_ALL, []), + [ + ("local", OPT_SWITCH, :devdir => true), + ("shared", OPT_SWITCH, :devdir => false), + ], + md""" + develop [--shared|--local] pkg[=uuid] [#rev] ... + +Make a package available for development. If `pkg` is an existing local path that path will be recorded in +the manifest and used. Otherwise, a full git clone of `pkg` at rev `rev` is made. The location of the clone is +controlled by the `--shared` (default) and `--local` arguments. The `--shared` location defaults to +`~/.julia/dev`, but can be controlled with the `JULIA_PKG_DEVDIR` environment variable. When `--local` is given, +the clone is placed in a `dev` folder in the current project. +This operation is undone by `free`. + +*Example* +```jl +pkg> develop Example +pkg> develop Example#master +pkg> develop Example#c37b675 +pkg> develop https://github.com/JuliaLang/Example.jl#master +pkg> develop --local Example +``` + """, +),( CMD_FREE, + ["free"], + do_free!, + (ARG_PKG, []), + [], + md""" + free pkg[=uuid] ... + +Free a pinned package `pkg`, which allows it to be upgraded or downgraded again. If the package is checked out (see `help develop`) then this command +makes the package no longer being checked out. + """, +),( CMD_PIN, + ["pin"], + do_pin!, + (ARG_VERSION, []), + [], + md""" + + pin pkg[=uuid] ... + +Pin packages to given versions, or the current version if no version is specified. A pinned package has its version fixed and will not be upgraded or downgraded. +A pinned package has the symbol `⚲` next to its version in the status list. + """, +),( CMD_BUILD, + ["build"], + do_build!, + (ARG_PKG, []), + [], + md""" + + build pkg[=uuid] ... + +Run the build script in `deps/build.jl` for each package in `pkg` and all of their dependencies in depth-first recursive order. +If no packages are given, runs the build scripts for all packages in the manifest. +The `startup.jl` file is disabled during building unless julia is started with `--startup-file=yes`. + """, +),( CMD_RESOLVE, + ["resolve"], + do_resolve!, + (ARG_RAW, [0]), + [], + md""" + resolve + +Resolve the project i.e. run package resolution and update the Manifest. This is useful in case the dependencies of developed +packages have changed causing the current Manifest to_indices be out of sync. + """, +),( CMD_ACTIVATE, + ["activate"], + do_activate!, + (ARG_RAW, [0,1]), + [], + nothing, +),( CMD_UP, + ["update", "up"], + do_up!, + (ARG_VERSION, []), + [ + (["project", "p"], OPT_SWITCH, :mode => PKGMODE_PROJECT), + (["manifest", "m"], OPT_SWITCH, :mode => PKGMODE_MANIFEST), + ("major", OPT_SWITCH, :level => UPLEVEL_MAJOR), + ("minor", OPT_SWITCH, :level => UPLEVEL_MINOR), + ("patch", OPT_SWITCH, :level => UPLEVEL_PATCH), + ("fixed", OPT_SWITCH, :level => UPLEVEL_FIXED), + ], + md""" + + up [-p|project] [opts] pkg[=uuid] [@version] ... + up [-m|manifest] [opts] pkg[=uuid] [@version] ... + + opts: --major | --minor | --patch | --fixed + +Update the indicated package within the constraints of the indicated version +specifications. Versions may be specified by `@1`, `@1.2`, `@1.2.3`, allowing +any version with a prefix that matches, or ranges thereof, such as `@1.2-3.4.5`. +In `--project` mode, package specifications only match project packages, while +in `manifest` mode they match any manifest package. Bound level options force +the following packages to be upgraded only within the current major, minor, +patch version; if the `--fixed` upgrade level is given, then the following +packages will not be upgraded at all. + """, +),( CMD_GENERATE, + ["generate"], + do_generate!, + (ARG_RAW, [1]), + [], + md""" + + generate pkgname + +Create a project called `pkgname` in the current folder. + """, +),( CMD_PRECOMPILE, + ["precompile"], + do_precompile!, + (ARG_RAW, [0]), + [], + md""" + precompile + +Precompile all the dependencies of the project by running `import` on all of them in a new process. +The `startup.jl` file is disabled during precompilation unless julia is started with `--startup-file=yes`. + """, +),( CMD_STATUS, + ["status", "st"], + do_status!, + (ARG_RAW, [0]), + [ + (["project", "p"], OPT_SWITCH, :mode => PKGMODE_PROJECT), + (["manifest", "m"], OPT_SWITCH, :mode => PKGMODE_MANIFEST), + ], + md""" + + status + status [-p|--project] + status [-m|--manifest] + +Show the status of the current environment. By default, the full contents of +the project file is summarized, showing what version each package is on and +how it has changed since the last git commit (if in a git repo), as well as +any changes to manifest packages not already listed. In `--project` mode, the +status of the project file is summarized. In `--manifest` mode the output also +includes the dependencies of explicitly added packages. + """, +),( CMD_GC, + ["gc"], + do_gc!, + (ARG_RAW, [0]), + [], + md""" + +Deletes packages that cannot be reached from any existing environment. + """, +),( CMD_PREVIEW, + ["preview"], + do_preview!, + (ARG_RAW, [1]), + [], + md""" + + preview cmd + +Runs the command `cmd` in preview mode. This is defined such that no side effects +will take place i.e. no packages are downloaded and neither the project nor manifest +is modified. + """, +), +], #package +] #command_declarations + +super_specs = SuperSpecs(command_declarations) # TODO should this go here ? +command_specs = super_specs["package"] +all_commands_sorted = sort(collect(String,keys(command_specs))) +long_commands = filter(c -> length(c) > 2, all_commands_sorted) +function all_options() + all_opts = [] + for command in values(command_specs) + for opt_spec in values(command.option_specs) + push!(all_opts, opt_spec.name) + opt_spec.short_name !== nothing && push!(all_opts, opt_spec.short_name) + end + end + unique!(all_opts) + return all_opts end +all_options_sorted = [length(opt) > 1 ? "--$opt" : "-$opt" for opt in sort!(all_options())] +long_options = filter(c -> length(c) > 2, all_options_sorted) + +const help = md""" + +**Welcome to the Pkg REPL-mode**. To return to the `julia>` prompt, either press +backspace when the input line is empty or press Ctrl+C. + + +**Synopsis** + + pkg> [--env=...] cmd [opts] [args] + +Multiple commands can be given on the same line by interleaving a `;` between the commands. + +**Environment** + +The `--env` meta option determines which project environment to manipulate. By +default, this looks for a git repo in the parents directories of the current +working directory, and if it finds one, it uses that as an environment. Otherwise, +it uses a named environment (typically found in `~/.julia/environments`) looking +for environments named `v$(VERSION.major).$(VERSION.minor).$(VERSION.patch)`, +`v$(VERSION.major).$(VERSION.minor)`, `v$(VERSION.major)` or `default` in order. + +**Commands** + +What action you want the package manager to take: + +`help`: show this message + +`status`: summarize contents of and changes to environment + +`add`: add packages to project + +`develop`: clone the full package repo locally for development + +`rm`: remove packages from project or manifest + +`up`: update packages in manifest + +`test`: run tests for packages + +`build`: run the build script for packages + +`pin`: pins the version of packages + +`free`: undoes a `pin`, `develop`, or stops tracking a repo. + +`instantiate`: downloads all the dependencies for the project + +`resolve`: resolves to update the manifest from changes in dependencies of +developed packages + +`generate`: generate files for a new project + +`preview`: previews a subsequent command without affecting the current state + +`precompile`: precompile all the project dependencies + +`gc`: garbage collect packages not used for a significant time + +`activate`: set the primary environment the package manager manipulates +""" + +end #module diff --git a/stdlib/Pkg/test/repl.jl b/stdlib/Pkg/test/repl.jl index 2530a4ed5709a8..34e7ad9f759189 100644 --- a/stdlib/Pkg/test/repl.jl +++ b/stdlib/Pkg/test/repl.jl @@ -72,23 +72,27 @@ temp_pkg_dir() do project_path end @testset "tokens" begin - tokens = Pkg.REPLMode.tokenize("add git@github.com:JuliaLang/Example.jl.git") - @test tokens[1][2] == "git@github.com:JuliaLang/Example.jl.git" - tokens = Pkg.REPLMode.tokenize("add git@github.com:JuliaLang/Example.jl.git#master") - @test tokens[1][2] == "git@github.com:JuliaLang/Example.jl.git" - @test tokens[1][3].rev == "master" - tokens = Pkg.REPLMode.tokenize("add git@github.com:JuliaLang/Example.jl.git#c37b675") - @test tokens[1][2] == "git@github.com:JuliaLang/Example.jl.git" - @test tokens[1][3].rev == "c37b675" - tokens = Pkg.REPLMode.tokenize("add git@github.com:JuliaLang/Example.jl.git@v0.5.0") - @test tokens[1][2] == "git@github.com:JuliaLang/Example.jl.git" - @test repr(tokens[1][3]) == "VersionRange(\"0.5.0\")" - tokens = Pkg.REPLMode.tokenize("add git@github.com:JuliaLang/Example.jl.git@0.5.0") - @test tokens[1][2] == "git@github.com:JuliaLang/Example.jl.git" - @test repr(tokens[1][3]) == "VersionRange(\"0.5.0\")" - tokens = Pkg.REPLMode.tokenize("add git@gitlab-fsl.jsc.näsan.guvv:drats/URGA2010.jl.git@0.5.0") - @test tokens[1][2] == "git@gitlab-fsl.jsc.näsan.guvv:drats/URGA2010.jl.git" - @test repr(tokens[1][3]) == "VersionRange(\"0.5.0\")" + statement = Pkg.REPLMode.parse("add git@github.com:JuliaLang/Example.jl.git")[1] + @test "add" in statement.command.names + @test statement.arguments[1] == "git@github.com:JuliaLang/Example.jl.git" + statement = Pkg.REPLMode.parse("add git@github.com:JuliaLang/Example.jl.git#master")[1] + @test "add" in statement.command.names + @test length(statement.arguments) == 2 + @test statement.arguments[1] == "git@github.com:JuliaLang/Example.jl.git" + @test statement.arguments[2] == "#master" + statement = Pkg.REPLMode.parse("add git@github.com:JuliaLang/Example.jl.git#c37b675")[1] + @test "add" in statement.command.names + @test length(statement.arguments) == 2 + @test statement.arguments[1] == "git@github.com:JuliaLang/Example.jl.git" + @test statement.arguments[2] == "#c37b675" + statement = Pkg.REPLMode.parse("add git@github.com:JuliaLang/Example.jl.git@v0.5.0")[1] + @test statement.arguments[1] == "git@github.com:JuliaLang/Example.jl.git" + @test statement.arguments[2] == "@v0.5.0" + statement = Pkg.REPLMode.parse("add git@gitlab-fsl.jsc.näsan.guvv:drats/URGA2010.jl.git@0.5.0")[1] + @test "add" in statement.command.names + @test length(statement.arguments) == 2 + @test statement.arguments[1] == "git@gitlab-fsl.jsc.näsan.guvv:drats/URGA2010.jl.git" + @test statement.arguments[2] == "@0.5.0" end temp_pkg_dir() do project_path; cd(project_path) do; mktempdir() do tmp_pkg_path @@ -566,10 +570,301 @@ end @testset "`do_generate!` error paths" begin with_temp_env() do - @test_throws CommandError Pkg.REPLMode.pkgstr("generate @0.0.0") @test_throws CommandError Pkg.REPLMode.pkgstr("generate Example Example2") @test_throws CommandError Pkg.REPLMode.pkgstr("generate") end end +@testset "`parse_option` unit tests" begin + opt = Pkg.REPLMode.parse_option("-x") + @test opt.val == "x" + @test opt.argument === nothing + opt = Pkg.REPLMode.parse_option("--hello") + @test opt.val == "hello" + @test opt.argument === nothing + opt = Pkg.REPLMode.parse_option("--env=some") + @test opt.val == "env" + @test opt.argument == "some" +end + +@testset "`parse` integration tests" begin + @test isempty(Pkg.REPLMode.parse("")) + + statement = Pkg.REPLMode.parse("up")[1] + @test statement.command.kind == Pkg.REPLMode.CMD_UP + @test isempty(statement.meta_options) + @test isempty(statement.options) + @test isempty(statement.arguments) + + statement = Pkg.REPLMode.parse("dev Example")[1] + @test statement.command.kind == Pkg.REPLMode.CMD_DEVELOP + @test isempty(statement.meta_options) + @test isempty(statement.options) + @test statement.arguments == ["Example"] + + statement = Pkg.REPLMode.parse("dev Example#foo #bar")[1] + @test statement.command.kind == Pkg.REPLMode.CMD_DEVELOP + @test isempty(statement.meta_options) + @test isempty(statement.options) + @test statement.arguments == ["Example", "#foo", "#bar"] + + statement = Pkg.REPLMode.parse("dev Example#foo Example@v0.0.1")[1] + @test statement.command.kind == Pkg.REPLMode.CMD_DEVELOP + @test isempty(statement.meta_options) + @test isempty(statement.options) + @test statement.arguments == ["Example", "#foo", "Example", "@v0.0.1"] + + statement = Pkg.REPLMode.parse("--one -t add --first --second arg1")[1] + @test statement.command.kind == Pkg.REPLMode.CMD_ADD + @test statement.meta_options == ["--one", "-t"] + @test statement.options == ["--first", "--second"] + @test statement.arguments == ["arg1"] + + statements = Pkg.REPLMode.parse("--one -t add --first -o arg1; --meta pin -x -a arg0 Example") + @test statements[1].command.kind == Pkg.REPLMode.CMD_ADD + @test statements[1].meta_options == ["--one", "-t"] + @test statements[1].options == ["--first", "-o"] + @test statements[1].arguments == ["arg1"] + @test statements[2].command.kind == Pkg.REPLMode.CMD_PIN + @test statements[2].meta_options == ["--meta"] + @test statements[2].options == ["-x", "-a"] + @test statements[2].arguments == ["arg0", "Example"] + + statements = Pkg.REPLMode.parse("up; --meta -x pin --first; dev") + @test statements[1].command.kind == Pkg.REPLMode.CMD_UP + @test isempty(statements[1].meta_options) + @test isempty(statements[1].options) + @test isempty(statements[1].arguments) + @test statements[2].command.kind == Pkg.REPLMode.CMD_PIN + @test statements[2].meta_options == ["--meta", "-x"] + @test statements[2].options == ["--first"] + @test isempty(statements[2].arguments) + @test statements[3].command.kind == Pkg.REPLMode.CMD_DEVELOP + @test isempty(statements[3].meta_options) + @test isempty(statements[3].options) + @test isempty(statements[3].arguments) +end + +@testset "argument count errors" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("activate one two") + @test_throws CommandError Pkg.REPLMode.pkgstr("activate one two three") + @test_throws CommandError Pkg.REPLMode.pkgstr("precompile Example") + end + end + end +end + +@testset "invalid options" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("rm --minor Example") + @test_throws CommandError Pkg.REPLMode.pkgstr("pin --project Example") + end + end + end +end + +@testset "Argument order" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("add FooBar Example#foobar#foobar") + @test_throws CommandError Pkg.REPLMode.pkgstr("up Example#foobar@0.0.0") + @test_throws CommandError Pkg.REPLMode.pkgstr("pin Example@0.0.0@0.0.1") + @test_throws CommandError Pkg.REPLMode.pkgstr("up #foobar") + @test_throws CommandError Pkg.REPLMode.pkgstr("add @0.0.1") + end + end + end +end + +@testset "conflicting options" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("up --major --minor") + @test_throws CommandError Pkg.REPLMode.pkgstr("rm --project --manifest") + end + end + end +end + +@testset "gc" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("gc --project") + @test_throws CommandError Pkg.REPLMode.pkgstr("gc --minor") + @test_throws CommandError Pkg.REPLMode.pkgstr("gc Example") + Pkg.REPLMode.pkgstr("gc") + end + end + end +end + +@testset "precompile" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("precompile --project") + @test_throws CommandError Pkg.REPLMode.pkgstr("precompile Example") + Pkg.REPLMode.pkgstr("precompile") + end + end + end +end + +@testset "generate" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("generate --major Example") + @test_throws CommandError Pkg.REPLMode.pkgstr("generate --foobar Example") + @test_throws CommandError Pkg.REPLMode.pkgstr("generate Example1 Example2") + Pkg.REPLMode.pkgstr("generate Example") + end + end + end +end + +@testset "test" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + Pkg.add("Example") + @test_throws CommandError Pkg.REPLMode.pkgstr("test --project Example") + Pkg.REPLMode.pkgstr("test --coverage Example") + Pkg.REPLMode.pkgstr("test Example") + end + end + end +end + +@testset "build" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("build --project") + @test_throws CommandError Pkg.REPLMode.pkgstr("build --minor") + end + end + end +end + +@testset "free" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError Pkg.REPLMode.pkgstr("free --project") + @test_throws CommandError Pkg.REPLMode.pkgstr("free --major") + end + end + end +end + +@testset "unit tests for `group_words`" begin + # simple + groups = Pkg.REPLMode.group_words(["add", "Example"]) + @test length(groups) == 1 + @test groups[1][1] == "add" + @test groups[1][2] == "Example" + # statement break + groups = Pkg.REPLMode.group_words(["a", "b", "c", ";", "a", "b"]) + @test length(groups) == 2 + groups = Pkg.REPLMode.group_words(["a", "b", "c", ";", "a", "b", ";", "d"]) + @test length(groups) == 3 + # trailing statement break + groups = Pkg.REPLMode.group_words(["a", "b", "c", ";", "a", "b", ";"]) + @test length(groups) == 2 + # errors + @test_throws CommandError Pkg.REPLMode.group_words(["a", "b", ";", ";", "a", "b"]) + @test_throws CommandError Pkg.REPLMode.group_words([";", "add", "Example"]) +end + +@testset "tests for api opts" begin + specs = Pkg.REPLMode.OptionSpecs(Pkg.REPLMode.OptionDeclaration[ + (["project", "p"], Pkg.REPLMode.OPT_SWITCH, :mode => Pkg.Types.PKGMODE_PROJECT), + (["manifest", "m"], Pkg.REPLMode.OPT_SWITCH, :mode => Pkg.Types.PKGMODE_MANIFEST), + ("major", Pkg.REPLMode.OPT_SWITCH, :level => Pkg.Types.UPLEVEL_MAJOR), + ("minor", Pkg.REPLMode.OPT_SWITCH, :level => Pkg.Types.UPLEVEL_MINOR), + ("patch", Pkg.REPLMode.OPT_SWITCH, :level => Pkg.Types.UPLEVEL_PATCH), + ("fixed", Pkg.REPLMode.OPT_SWITCH, :level => Pkg.Types.UPLEVEL_FIXED), + ("rawnum", Pkg.REPLMode.OPT_ARG, :num => nothing), + ("plus", Pkg.REPLMode.OPT_ARG, :num => x->parse(Int,x)+1), + ]) + + api_opts = Pkg.REPLMode.APIOptions([ + Pkg.REPLMode.Option("manifest"), + Pkg.REPLMode.Option("patch"), + Pkg.REPLMode.Option("rawnum", "5"), + ], specs) + + @test Pkg.REPLMode.key_api(:foo, api_opts) === nothing + @test Pkg.REPLMode.key_api(:mode, api_opts) == Pkg.Types.PKGMODE_MANIFEST + @test Pkg.REPLMode.key_api(:level, api_opts) == Pkg.Types.UPLEVEL_PATCH + @test Pkg.REPLMode.key_api(:num, api_opts) == "5" + + api_opts = Pkg.REPLMode.APIOptions([ + Pkg.REPLMode.Option("project"), + Pkg.REPLMode.Option("patch"), + Pkg.REPLMode.Option("plus", "5"), + ], specs) + + @test Pkg.REPLMode.key_api(:mode, api_opts) == Pkg.Types.PKGMODE_PROJECT + @test Pkg.REPLMode.key_api(:level, api_opts) == Pkg.Types.UPLEVEL_PATCH + @test Pkg.REPLMode.key_api(:num, api_opts) == 6 + + @test Pkg.REPLMode.key_api(:foo, api_opts) === nothing + Pkg.REPLMode.set_default!(:foo => "bar", api_opts) + @test Pkg.REPLMode.key_api(:foo, api_opts) == "bar" + Pkg.REPLMode.set_default!(:level => "bar", api_opts) + @test Pkg.REPLMode.key_api(:level, api_opts) == Pkg.Types.UPLEVEL_PATCH +end + +@testset "meta option errors" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + # unregistered meta options + @test_throws CommandError Pkg.REPLMode.pkgstr("--foo=foo add Example") + @test_throws CommandError Pkg.REPLMode.pkgstr("--bar add Example") + @test_throws CommandError Pkg.REPLMode.pkgstr("-x add Example") + # malformed, but registered meta option + @test_throws CommandError Pkg.REPLMode.pkgstr("--env Example") + end + end + end +end + +@testset "activate" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + mkdir("Foo") + pkg"activate" + default = Base.active_project() + pkg"activate Foo" + @test Base.active_project() == joinpath(pwd(), "Foo", "Project.toml") + pkg"activate" + @test Base.active_project() == default + end + end + end +end + +@testset "subcommands" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do + Pkg.REPLMode.pkg"package add Example" + @test isinstalled(TEST_PKG) + Pkg.REPLMode.pkg"package rm Example" + @test !isinstalled(TEST_PKG) + end + end + end +end + +@testset "`parse_quotes` unit tests" begin + qwords = Pkg.REPLMode.parse_quotes("\"Don't\" forget to '\"test\"'") + @test qwords[1].isquoted + @test qwords[1].word == "Don't" + @test !qwords[2].isquoted + @test qwords[2].word == "forget" + @test !qwords[3].isquoted + @test qwords[3].word == "to" + @test qwords[4].isquoted + @test qwords[4].word == "\"test\"" + @test_throws CommandError Pkg.REPLMode.parse_quotes("Don't") + @test_throws CommandError Pkg.REPLMode.parse_quotes("Unterminated \"quot") +end + +@testset "argument kinds" begin + temp_pkg_dir() do project_path; cd_tempdir() do tmpdir; with_temp_env() do; + @test_throws CommandError pkg"pin Example#foo" + @test_throws CommandError pkg"test Example#foo" + @test_throws CommandError pkg"test Example@v0.0.1" + end + end + end +end + end # module