help.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. const { spawn } = require('child_process')
  2. const path = require('path')
  3. const openUrl = require('./utils/open-url.js')
  4. const { promisify } = require('util')
  5. const glob = promisify(require('glob'))
  6. const localeCompare = require('@isaacs/string-locale-compare')('en')
  7. const BaseCommand = require('./base-command.js')
  8. // Strips out the number from foo.7 or foo.7. or foo.7.tgz
  9. // We don't currently compress our man pages but if we ever did this would
  10. // seemlessly continue supporting it
  11. const manNumberRegex = /\.(\d+)(\.[^/\\]*)?$/
  12. class Help extends BaseCommand {
  13. /* istanbul ignore next - see test/lib/load-all-commands.js */
  14. static get description () {
  15. return 'Get help on npm'
  16. }
  17. /* istanbul ignore next - see test/lib/load-all-commands.js */
  18. static get name () {
  19. return 'help'
  20. }
  21. /* istanbul ignore next - see test/lib/load-all-commands.js */
  22. static get usage () {
  23. return ['<term> [<terms..>]']
  24. }
  25. /* istanbul ignore next - see test/lib/load-all-commands.js */
  26. static get params () {
  27. return ['viewer']
  28. }
  29. async completion (opts) {
  30. if (opts.conf.argv.remain.length > 2)
  31. return []
  32. const g = path.resolve(__dirname, '../man/man[0-9]/*.[0-9]')
  33. const files = await glob(g)
  34. return Object.keys(files.reduce(function (acc, file) {
  35. file = path.basename(file).replace(/\.[0-9]+$/, '')
  36. file = file.replace(/^npm-/, '')
  37. acc[file] = true
  38. return acc
  39. }, { help: true }))
  40. }
  41. exec (args, cb) {
  42. this.help(args).then(() => cb()).catch(cb)
  43. }
  44. async help (args) {
  45. // By default we search all of our man subdirectories, but if the user has
  46. // asked for a specific one we limit the search to just there
  47. let manSearch = 'man*'
  48. if (/^\d+$/.test(args[0]))
  49. manSearch = `man${args.shift()}`
  50. if (!args.length)
  51. return this.npm.output(this.npm.usage)
  52. // npm help foo bar baz: search topics
  53. if (args.length > 1)
  54. return this.helpSearch(args)
  55. let section = this.npm.deref(args[0]) || args[0]
  56. // support `npm help package.json`
  57. section = section.replace('.json', '-json')
  58. const manroot = path.resolve(__dirname, '..', 'man')
  59. // find either section.n or npm-section.n
  60. const f = `${manroot}/${manSearch}/?(npm-)${section}.[0-9]*`
  61. let mans = await glob(f)
  62. mans = mans.sort((a, b) => {
  63. // Because of the glob we know the manNumberRegex will pass
  64. const aManNumber = a.match(manNumberRegex)[1]
  65. const bManNumber = b.match(manNumberRegex)[1]
  66. // man number sort first so that 1 aka commands are preferred
  67. if (aManNumber !== bManNumber)
  68. return aManNumber - bManNumber
  69. return localeCompare(a, b)
  70. })
  71. const man = mans[0]
  72. if (man)
  73. await this.viewMan(man)
  74. else
  75. return this.helpSearch(args)
  76. }
  77. helpSearch (args) {
  78. return new Promise((resolve, reject) => {
  79. this.npm.commands['help-search'](args, (err) => {
  80. // This would only error if args was empty, which it never is
  81. /* istanbul ignore next */
  82. if (err)
  83. return reject(err)
  84. resolve()
  85. })
  86. })
  87. }
  88. async viewMan (man) {
  89. const env = {}
  90. Object.keys(process.env).forEach(function (i) {
  91. env[i] = process.env[i]
  92. })
  93. const viewer = this.npm.config.get('viewer')
  94. const opts = {
  95. env,
  96. stdio: 'inherit',
  97. }
  98. let bin = 'man'
  99. const args = []
  100. switch (viewer) {
  101. case 'woman':
  102. bin = 'emacsclient'
  103. args.push('-e', `(woman-find-file '${man}')`)
  104. break
  105. case 'browser':
  106. await openUrl(this.npm, this.htmlMan(man), 'help available at the following URL')
  107. return
  108. default:
  109. args.push(man)
  110. break
  111. }
  112. const proc = spawn(bin, args, opts)
  113. return new Promise((resolve, reject) => {
  114. proc.on('exit', (code) => {
  115. if (code)
  116. return reject(new Error(`help process exited with code: ${code}`))
  117. return resolve()
  118. })
  119. })
  120. }
  121. // Returns the path to the html version of the man page
  122. htmlMan (man) {
  123. let sect = man.match(manNumberRegex)[1]
  124. const f = path.basename(man).replace(manNumberRegex, '')
  125. switch (sect) {
  126. case '1':
  127. sect = 'commands'
  128. break
  129. case '5':
  130. sect = 'configuring-npm'
  131. break
  132. case '7':
  133. sect = 'using-npm'
  134. break
  135. }
  136. return 'file://' + path.resolve(__dirname, '..', 'docs', 'output', sect, f + '.html')
  137. }
  138. }
  139. module.exports = Help