diff --git a/.editorconfig b/.editorconfig index 9a2c1b470b..9e6433f99f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,7 +15,7 @@ trim_trailing_whitespace = false indent_style = tab indent_size = 8 -[{Dockerfile,Makefile,*.go}] +[{Dockerfile,Makefile,*.go,script/add-grammar}] indent_style = tab indent_size = 4 diff --git a/script/add-grammar b/script/add-grammar index 75d168cb19..97b970aa97 100755 --- a/script/add-grammar +++ b/script/add-grammar @@ -1,124 +1,139 @@ -#!/usr/bin/env ruby - -require "optparse" -require "open3" - -ROOT = File.expand_path("../../", __FILE__) - - -# Break a repository URL into its separate components -def parse_url(input) - hosts = "github\.com|bitbucket\.org|gitlab\.com" - - # HTTPS/HTTP link pointing to recognised hosts - if input =~ /^(?:https?:\/\/)?(?:[^.@]+@)?(?:www\.)?(#{hosts})\/([^\/]+)\/([^\/]+)/i - { host: $1.downcase(), user: $2, repo: $3.sub(/\.git$/, "") } - # SSH - elsif input =~ /^git@(#{hosts}):([^\/]+)\/([^\/]+)\.git$/i - { host: $1.downcase(), user: $2, repo: $3 } - # provider:user/repo - elsif input =~ /^(github|bitbucket|gitlab):\/?([^\/]+)\/([^\/]+)\/?$/i - { host: $1.downcase(), user: $2, repo: $3 } - # user/repo - Common GitHub shorthand - elsif input =~ /^\/?([^\/]+)\/([^\/]+)\/?$/ - { host: "github.com", user: $1, repo: $2 } - else - raise "Unsupported URL: #{input}" - end -end - -# Isolate the vendor-name component of a submodule path -def parse_submodule(name) - name =~ /^(?:.*(?:vendor\/)?grammars\/)?([^\/]+)/i - path = "vendor/grammars/#{$1}" - unless File.exist?("#{ROOT}/" + path) - warn "Submodule '#{path}' does not exist. Aborting." - exit 1 - end - path -end - -# Print debugging feedback to STDOUT if not running with --quiet -def log(msg) - puts msg if $verbose -end - -def command(*args, hide_warnings: false) - log "$ #{args.join(' ')}" - stdout, stderr, status = Open3.capture3(*args) - unless status.success? - output = stdout.strip + "\n" + stderr.strip - output.each_line do |line| - log " > #{line}" - end - warn "Command failed. Aborting." - exit 1 - end - return if stderr.empty? - - unless hide_warnings - stderr.each_line do |line| - log " > #{line}" - end - end -end - -usage = """Usage: - #{$0} [-q|--quiet] [--replace grammar] url -Examples: - #{$0} https://github.com/Alhadis/language-roff - #{$0} --replace sublime-apl https://github.com/Alhadis/language-apl -""" - -$replace = nil -$verbose = true - -OptionParser.new do |opts| - opts.banner = usage - opts.on("-q", "--quiet", "Do not print output unless there's a failure") do - $verbose = false - end - opts.on("-rSUBMODULE", "--replace=SUBMODDULE", "Replace an existing grammar submodule.") do |name| - $replace = name - end -end.parse! - - -$url = ARGV[0] - -# No URL? Print a usage message and bail. -unless $url - warn usage - exit 1; -end - -# Exit early if docker isn't installed or running. -log "Checking docker is installed and running" -command('docker', 'ps') +#!/bin/sh +# shellcheck disable=SC2006,SC2021 +set -e + +usage="${0##*/} [-q|--quiet] [--replace submodule] url" +unset replace quiet + +# Print non-essential feedback +log()([ "$quiet" ] || printf '%s\n' "$@") + +# Print an error message +warn()(printf '%s: %s\n' "${0##*/}" "$@") + +# Display a shortened help summary and bail with an error code +bad_invocation(){ + printf '%s\n' "$usage" >&2 + exit 2 +} + + +# Parse options +while [ -n "$1" ]; do case $1 in + + # Print an unabridged usage summary, then exit + -h|--help|-\?) + cat <<-HELP + Usage: + $usage + + Options: + -q, --quiet Do not print output unless there's a failure. + -r, --replace SUBMODDULE Replace an existing grammar submodule. + + Examples: + $0 https://github.com/Alhadis/language-roff + $0 --replace sublime-apl https://github.com/Alhadis/language-apl + HELP + exit ;; + + # Hide non-essential feedback + -q | --quiet) + quiet=1 + break ;; + + # Replace an existing submodule + -r* | --replace | --replace=*) + case $1 in + -r|--replace) replace=$2; shift ;; # -r [module], --replace [module] + -r*) replace=${1#??} ;; # -r[module] + --replace=*) replace=${1#*=} ;; # --replace=[module] + esac ;; + + # Double-dash: Terminate option parsing + --) + shift + break ;; + + # Invalid option: abort + --* | -?*) + warn 'invalid option: "%s"' "${0##*/}" "$1" >&2 + bad_invocation ;; + + # Argument not prefixed with a dash + *) break ;; + +esac; shift +done + + +# Don't proceed any further if we don't have a URL +[ "$1" ] || bad_invocation + + +# Check upfront that executables we depend on are available +for cmd in docker git sed ruby bundle; do + command -v "$cmd" >/dev/null 2>&1 || { + warn "Required command '$cmd' not found" + warn 'See CONTRIBUTING.md for help on getting started: https://git.io/J0eqy' + exit 1 + } +done + +# Make sure Docker's running +log 'Checking Docker is installed and running' +docker ps >/dev/null + +# Make sure we're running from checkout directory +root=`git rev-parse --git-dir` +root="${root%/.git}" +# shellcheck disable=SC3013 +if [ ! "$root" = .git ] && [ -d "$root" ] && ! [ . -ef "$root" ] >/dev/null 2>&1; then + log "Switching directory to $root" + cd "$root" +fi # Ensure the given URL is an HTTPS link -parts = parse_url $url -https = "https://#{parts[:host]}/#{parts[:user]}/#{parts[:repo]}" -repo_new = "vendor/grammars/#{parts[:repo]}" -repo_old = parse_submodule($replace) if $replace - -Dir.chdir(ROOT) - -if repo_old - log "Deregistering: #{repo_old}" - command('git', 'submodule', 'deinit', repo_old) - command('git', 'rm', '-rf', repo_old) - command('script/grammar-compiler', 'update', '-f', hide_warnings: true) -end - -log "Registering new submodule: #{repo_new}" -command('git', 'submodule', 'add', '-f', https, repo_new) -command('script/grammar-compiler', 'add', repo_new) - -log "Caching grammar license" -command("bundle", "exec", "licensed", "cache", "-c", "vendor/licenses/config.yml") - -log "Updating grammar documentation in vendor/README.md" -command('bundle', 'exec', 'rake', 'samples', hide_warnings: true) -command('script/sort-submodules') -command('script/list-grammars') +url=`script/normalise-url --protocol=https "$1"` + +# Make sure it's not already registered +path="vendor/grammars/${url##*/}" +path="${path%.git}" +if [ -e "$path" ]; then + warn "Submodule '$path' already exists. Did you forget the '--replace' option?" + warn "Run '$0 --help' for invocation advice" + exit 1 +fi + +# Remove the old submodule if we're `--replace`ing one +if [ "$replace" ]; then + + # Normalise submodule reference + replace=`printf %s "$replace" \ + | tr '[A-Z]' '[a-z]' \ + | sed 's/^\(.*\/\)\{0,1\}vendor\///; s/^grammars\///'` + replace=`git config --list \ + | grep -Fi -m1 "submodule.vendor/grammars/$replace.url=" \ + | sed 's/\.url=.*//; s/^submodule\.//' || :` + [ "$replace" ] || { + warn "Submodule '$replace' does not exist. Aborting." + exit 1 + } + + log "Deregistering submodule: $replace" + git submodule deinit "$replace" + git rm -rf "$replace" + script/grammar-compiler update -f >/dev/null 2>&1 +fi + +log "Registering new submodule: $url" +git submodule add -f "$url" "$path" +script/grammar-compiler add "$path" + +log 'Caching grammar license' +bundle exec licensed cache -c vendor/licenses/config.yml + +log 'Updating grammar documentation in vendor/README.md' +bundle exec rake samples >/dev/null 2>&1 +script/sort-submodules +script/list-grammars diff --git a/script/normalise-url b/script/normalise-url new file mode 100755 index 0000000000..c2d48105dc --- /dev/null +++ b/script/normalise-url @@ -0,0 +1,143 @@ +#!/usr/bin/env ruby + +require "English" + +# Whitelisted grammar providers +HOSTS = %w[ + github.com + gitlab.com + bitbucket.org +] + +# Construct a well-formed URL from the output of `parse_url()` +def build_url(hash) + path = "#{hash[:user]}/#{hash[:repo]}.git" + if hash[:protocol] == "ssh" + "git@#{hash[:host]}:#{path}" + else + "#{hash[:protocol]}://#{hash[:host]}/#{path}" + end +end + +# Break a repository URL into its separate components +def parse_url(input) + hosts = %r[#{HOSTS.map { |x| Regexp.escape x }.join "|"}]i + + # HTTPS/HTTP link pointing to recognised hosts + case input + when %r[^ + (?: + (? https?|ssh|git) + (?:\+(?:git|ssh))? # Allowed, but deprecated + )? :* /* + (?:[^.@]*@)? + (?:www\.)? + (? #{hosts}) /+ + (? [^:@/]+) /+ + (? [^:@/]+?) + (?:\.git)? (?=$|[/#]) + ]ix; { + protocol: ($LAST_MATCH_INFO[:protocol] or "https").downcase, + host: $LAST_MATCH_INFO[:host].downcase, + user: $LAST_MATCH_INFO[:user], + repo: $LAST_MATCH_INFO[:repo] + } + + # SSH + when %r[^ + git (?:\+(?:ssh|https?))? @ + (?:www\.)? + (? #{hosts}) :/* + (? [^:@/]+) /+ + (? [^:@/]+?) + (?:\.git)? /*$ + ]ix; { + protocol: "ssh", + host: $LAST_MATCH_INFO[:host].downcase, + user: $LAST_MATCH_INFO[:user], + repo: $LAST_MATCH_INFO[:repo] + } + + # provider:user/repo + when %r[^ + (? + gh | github | + gl | gitlab | + bb | bitbucket + ) :/* + (? [^:@/]+) /+ + (? [^:@/]+?) + (?:\.git)? /*$ + ]ix; { + protocol: "https", + host: (case $LAST_MATCH_INFO[:host].downcase + when "gh", "github"; "github.com" + when "gl", "gitlab"; "gitlab.com" + when "bb", "bitbucket"; "bitbucket.org" + end), + user: $LAST_MATCH_INFO[:user], + repo: $LAST_MATCH_INFO[:repo] + } + + # user/repo - Common GitHub shorthand + when %r[^ + /* + (? [^:@/]+) /+ + (? [^:@/]+?) + (?:\.git)? /*$ + ]ix; { + protocol: "https", + host: "github.com", + user: $LAST_MATCH_INFO[:user], + repo: $LAST_MATCH_INFO[:repo] + } + + # Not something we recognise + else + raise "Unsupported URL: #{input}" + end +end + + +require "optparse" +$json = false +$protocol = nil + +OptionParser.new do |opts| + opts.banner = <<~END + #{$PROGRAM_NAME}: Resolve a repository URL from various formats + + Usage: + #{$PROGRAM_NAME} [-p|--protocol name] ...urls + #{$PROGRAM_NAME} [-j|--json] ...urls + #{$PROGRAM_NAME} [-h|--hosts] + + Examples: + $ #{$PROGRAM_NAME} Alhadis/language-etc BB:user/name + => https://github.com/Alhadis/language-etc.git + https://bitbucket.org/user/name.git + + Options: + END + opts.on("-h", "--hosts", "Print a list of whitelisted grammar hosts, then exit") do + puts HOSTS.join $RS + exit + end + opts.on("-j", "--json", "Output parsed URLs as an array of JSON objects") do + $json = true + end + opts.on("-pNAME", "--protocol=NAME", "Force URLs to use a protocol, even if different. Ignored for JSON output.") do |name| + $protocol = name.to_s.downcase + end +end.parse! + +if $json + require "json" + puts JSON.pretty_generate ARGV.map { |x| parse_url x }, { indent: "\t" } +else + ARGV.each do |arg| + url = parse_url arg + url[:protocol] = $protocol unless $protocol.nil? + puts build_url(url) + end +end