summaryrefslogtreecommitdiff
path: root/deps/npm/lib/commands/help.js
blob: 4b40ef37afa2def063d678c8d72982e332258e0e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const spawn = require('@npmcli/promise-spawn')
const path = require('path')
const openUrl = require('../utils/open-url.js')
const { glob } = require('glob')
const localeCompare = require('@isaacs/string-locale-compare')('en')

const globify = pattern => pattern.split('\\').join('/')
const BaseCommand = require('../base-command.js')

// Strips out the number from foo.7 or foo.7. or foo.7.tgz
// We don't currently compress our man pages but if we ever did this would
// seamlessly continue supporting it
const manNumberRegex = /\.(\d+)(\.[^/\\]*)?$/
// hardcoded names for mansections
// XXX: these are used in the docs workspace and should be exported
// from npm so section names can changed more easily
const manSectionNames = {
  1: 'commands',
  5: 'configuring-npm',
  7: 'using-npm',
}

class Help extends BaseCommand {
  static description = 'Get help on npm'
  static name = 'help'
  static usage = ['<term> [<terms..>]']
  static params = ['viewer']

  async completion (opts) {
    if (opts.conf.argv.remain.length > 2) {
      return []
    }
    const g = path.resolve(this.npm.npmRoot, 'man/man[0-9]/*.[0-9]')
    let files = await glob(globify(g))
    // preserve glob@8 behavior
    files = files.sort((a, b) => a.localeCompare(b, 'en'))

    return Object.keys(files.reduce(function (acc, file) {
      file = path.basename(file).replace(/\.[0-9]+$/, '')
      file = file.replace(/^npm-/, '')
      acc[file] = true
      return acc
    }, { help: true }))
  }

  async exec (args) {
    // By default we search all of our man subdirectories, but if the user has
    // asked for a specific one we limit the search to just there
    const manSearch = /^\d+$/.test(args[0]) ? `man${args.shift()}` : 'man*'

    if (!args.length) {
      return this.npm.output(await this.npm.usage)
    }

    // npm help foo bar baz: search topics
    if (args.length > 1) {
      return this.helpSearch(args)
    }

    // `npm help package.json`
    const arg = (this.npm.deref(args[0]) || args[0]).replace('.json', '-json')

    // find either section.n or npm-section.n
    const f = globify(path.resolve(this.npm.npmRoot, `man/${manSearch}/?(npm-)${arg}.[0-9]*`))

    const [man] = await glob(f).then(r => r.sort((a, b) => {
      // Because the glob is (subtly) different from manNumberRegex,
      // we can't rely on it passing.
      const aManNumberMatch = a.match(manNumberRegex)?.[1] || 999
      const bManNumberMatch = b.match(manNumberRegex)?.[1] || 999
      if (aManNumberMatch !== bManNumberMatch) {
        return aManNumberMatch - bManNumberMatch
      }
      return localeCompare(a, b)
    }))

    return man ? this.viewMan(man) : this.helpSearch(args)
  }

  helpSearch (args) {
    return this.npm.exec('help-search', args)
  }

  async viewMan (man) {
    const viewer = this.npm.config.get('viewer')

    if (viewer === 'browser') {
      return openUrl(this.npm, this.htmlMan(man), 'help available at the following URL', true)
    }

    let args = ['man', [man]]
    if (viewer === 'woman') {
      args = ['emacsclient', ['-e', `(woman-find-file '${man}')`]]
    }

    return spawn(...args, { stdio: 'inherit' }).catch(err => {
      if (err.code) {
        throw new Error(`help process exited with code: ${err.code}`)
      } else {
        throw err
      }
    })
  }

  // Returns the path to the html version of the man page
  htmlMan (man) {
    const sect = manSectionNames[man.match(manNumberRegex)[1]]
    const f = path.basename(man).replace(manNumberRegex, '')
    return 'file:///' + path.resolve(this.npm.npmRoot, `docs/output/${sect}/${f}.html`)
  }
}
module.exports = Help