@@ -11,81 +11,137 @@ export type EnvironmentName = string;
1111/**
1212 * Helpers for conda.
1313 */
14- export class CondaHelper {
15- /**
16- * Return the string to display for the conda interpreter.
17- */
18- public getDisplayName ( condaInfo : CondaInfo = { } ) : string {
19- // Samples.
20- // "3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]".
21- // "3.6.2 |Anaconda, Inc.| (default, Sep 21 2017, 18:29:43) \n[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]".
22- const sysVersion = condaInfo [ 'sys.version' ] ;
23- if ( ! sysVersion ) {
24- return AnacondaDisplayName ;
25- }
2614
27- // Take the second part of the sys.version.
28- const sysVersionParts = sysVersion . split ( '|' , 2 ) ;
29- if ( sysVersionParts . length === 2 ) {
30- const displayName = sysVersionParts [ 1 ] . trim ( ) ;
31- if ( this . isIdentifiableAsAnaconda ( displayName ) ) {
32- return displayName ;
33- } else {
34- return `${ displayName } : ${ AnacondaDisplayName } ` ;
35- }
15+ /**
16+ * Return the string to display for the conda interpreter.
17+ */
18+ export function getDisplayName ( condaInfo : CondaInfo = { } ) : string {
19+ // Samples.
20+ // "3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]".
21+ // "3.6.2 |Anaconda, Inc.| (default, Sep 21 2017, 18:29:43) \n[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]".
22+ const sysVersion = condaInfo [ 'sys.version' ] ;
23+ if ( ! sysVersion ) {
24+ return AnacondaDisplayName ;
25+ }
26+
27+ // Take the second part of the sys.version.
28+ const sysVersionParts = sysVersion . split ( '|' , 2 ) ;
29+ if ( sysVersionParts . length === 2 ) {
30+ const displayName = sysVersionParts [ 1 ] . trim ( ) ;
31+ if ( isIdentifiableAsAnaconda ( displayName ) ) {
32+ return displayName ;
3633 } else {
37- return AnacondaDisplayName ;
34+ return ` ${ displayName } : ${ AnacondaDisplayName } ` ;
3835 }
36+ } else {
37+ return AnacondaDisplayName ;
3938 }
39+ }
4040
41- /**
42- * Parses output returned by the command `conda env list`.
43- * Sample output is as follows:
44- * # conda environments:
45- * #
46- * base * /Users/donjayamanne/anaconda3
47- * one /Users/donjayamanne/anaconda3/envs/one
48- * one two /Users/donjayamanne/anaconda3/envs/one two
49- * py27 /Users/donjayamanne/anaconda3/envs/py27
50- * py36 /Users/donjayamanne/anaconda3/envs/py36
51- * three /Users/donjayamanne/anaconda3/envs/three
52- * /Users/donjayamanne/anaconda3/envs/four
53- * /Users/donjayamanne/anaconda3/envs/five 5
54- * @param {string } condaEnvironmentList
55- * @param {CondaInfo } condaInfo
56- * @returns {{ name: string, path: string }[] | undefined }
57- * @memberof CondaHelper
58- */
59- public parseCondaEnvironmentNames ( condaEnvironmentList : string ) : { name : string ; path : string } [ ] | undefined {
60- const environments = condaEnvironmentList . splitLines ( { trim : false } ) ;
61- const baseEnvironmentLine = environments . filter ( ( line ) => line . indexOf ( '*' ) > 0 ) ;
62- if ( baseEnvironmentLine . length === 0 ) {
63- return ;
41+ /**
42+ * Parses output returned by the command `conda env list`.
43+ * Sample output is as follows:
44+ * # conda environments:
45+ * #
46+ * base * /Users/donjayamanne/anaconda3
47+ * one /Users/donjayamanne/anaconda3/envs/one
48+ * py27 /Users/donjayamanne/anaconda3/envs/py27
49+ * py36 /Users/donjayamanne/anaconda3/envs/py36
50+ * three /Users/donjayamanne/anaconda3/envs/three
51+ * /Users/donjayamanne/anaconda3/envs/four
52+ * /Users/donjayamanne/anaconda3/envs/five 5
53+ * aaaa_bbbb_cccc_dddd_eeee_ffff_gggg /Users/donjayamanne/anaconda3/envs/aaaa_bbbb_cccc_dddd_eeee_ffff_gggg
54+ * with*star /Users/donjayamanne/anaconda3/envs/with*star
55+ * "/Users/donjayamanne/anaconda3/envs/seven "
56+ */
57+ export function parseCondaEnvFileContents (
58+ condaEnvFileContents : string
59+ ) : { name : string ; path : string ; isActive : boolean } [ ] | undefined {
60+ // Don't trim the lines. `path` portion of the line can end with a space.
61+ const lines = condaEnvFileContents . splitLines ( { trim : false } ) ;
62+ const envs : { name : string ; path : string ; isActive : boolean } [ ] = [ ] ;
63+
64+ lines . forEach ( ( line ) => {
65+ const item = parseCondaEnvFileLine ( line ) ;
66+ if ( item ) {
67+ envs . push ( item ) ;
6468 }
65- const pathStartIndex = baseEnvironmentLine [ 0 ] . indexOf ( baseEnvironmentLine [ 0 ] . split ( '*' ) [ 1 ] . trim ( ) ) ;
66- const envs : { name : string ; path : string } [ ] = [ ] ;
67- environments . forEach ( ( line ) => {
68- if ( line . length <= pathStartIndex ) {
69- return ;
70- }
71- let name = line . substring ( 0 , pathStartIndex ) . trim ( ) ;
72- if ( name . endsWith ( '*' ) ) {
73- name = name . substring ( 0 , name . length - 1 ) . trim ( ) ;
74- }
75- const envPath = line . substring ( pathStartIndex ) . trim ( ) ;
76- if ( envPath . length > 0 ) {
77- envs . push ( { name, path : envPath } ) ;
78- }
79- } ) ;
80-
81- return envs ;
69+ } ) ;
70+
71+ return envs . length > 0 ? envs : undefined ;
72+ }
73+
74+ function parseCondaEnvFileLine ( line : string ) : { name : string ; path : string ; isActive : boolean } | undefined {
75+ // Empty lines or lines starting with `#` are comments and can be ignored.
76+ if ( line . length === 0 || line . startsWith ( '#' ) ) {
77+ return undefined ;
78+ }
79+
80+ // This extraction is based on the following code for `conda env list`:
81+ // https://github.com/conda/conda/blob/f207a2114c388fd17644ee3a5f980aa7cf86b04b/conda/cli/common.py#L188
82+ // It uses "%-20s %s %s" as the format string. Where the middle %s is '*'
83+ // if the environment is active, and ' ' if it is not active.
84+
85+ // If conda environment was created using `-p` then it may NOT have a name.
86+ // Use empty string as default name for envs created using path only.
87+ let name = '' ;
88+ let remainder = line ;
89+
90+ // The `name` and `path` parts are separated by at least 5 spaces. We cannot
91+ // use a single space here since it can be part of the name (see below for
92+ // name spec). Another assumption here is that `name` does not start with
93+ // 5*spaces or somewhere in the center. However, ` name` or `a b` is
94+ // a valid name when using --clone. Highly unlikely that users will have this
95+ // form as the environment name. lastIndexOf() can also be used but that assumes
96+ // that `path` does NOT end with 5*spaces.
97+ let spaceIndex = line . indexOf ( ' ' ) ;
98+ if ( spaceIndex === - 1 ) {
99+ // This means the environment name is longer than 17 characters and it is
100+ // active. Try ' * ' for separator between name and path.
101+ spaceIndex = line . indexOf ( ' * ' ) ;
82102 }
83103
84- /**
85- * Does the given string match a known Anaconda identifier.
86- */
87- private isIdentifiableAsAnaconda ( value : string ) {
88- const valueToSearch = value . toLowerCase ( ) ;
89- return AnacondaIdentifiers . some ( ( item ) => valueToSearch . indexOf ( item . toLowerCase ( ) ) !== - 1 ) ;
104+ if ( spaceIndex > 0 ) {
105+ // Parsing `name`
106+ // > `conda create -n <name>`
107+ // conda environment `name` should NOT have following characters
108+ // ('/', ' ', ':', '#'). So we can use the index of 5*space
109+ // characters to extract the name.
110+ //
111+ // > `conda create --clone one -p "~/envs/one two"`
112+ // this can generate a cloned env with name `one two`. This is
113+ // only allowed for cloned environments. In both cases, the best
114+ // separator is 5*spaces. It is highly unlikely that users will have
115+ // 5*spaces in their environment name.
116+ //
117+ // Notes: When using clone if the path has a trailing space, it will
118+ // not be preserved for the name. Trailing spaces in environment names
119+ // are NOT allowed. But leading spaces are allowed. Currently there no
120+ // special separator character between name and path, other than spaces.
121+ // We will need a well known separator if this ever becomes a issue.
122+ name = line . substring ( 0 , spaceIndex ) . trimRight ( ) ;
123+ remainder = line . substring ( spaceIndex ) ;
90124 }
125+
126+ // Detecting Active Environment:
127+ // Only active environment will have `*` between `name` and `path`. `name`
128+ // or `path` can have `*` in them as well. So we have to look for `*` in
129+ // between `name` and `path`. We already extracted the name, the next non-
130+ // whitespace character should either be `*` or environment path.
131+ remainder = remainder . trimLeft ( ) ;
132+ const isActive = remainder . startsWith ( '*' ) ;
133+
134+ // Parsing `path`
135+ // If `*` is the first then we can skip that character. Trim left again,
136+ // don't do trim() or trimRight(), since paths can end with a space.
137+ remainder = ( isActive ? remainder . substring ( 1 ) : remainder ) . trimLeft ( ) ;
138+
139+ return { name, path : remainder , isActive } ;
140+ }
141+ /**
142+ * Does the given string match a known Anaconda identifier.
143+ */
144+ function isIdentifiableAsAnaconda ( value : string ) {
145+ const valueToSearch = value . toLowerCase ( ) ;
146+ return AnacondaIdentifiers . some ( ( item ) => valueToSearch . indexOf ( item . toLowerCase ( ) ) !== - 1 ) ;
91147}
0 commit comments