libdash 0.5.1__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {libdash-0.5.1 → libdash-0.5.3}/MANIFEST.in +1 -1
  2. {libdash-0.5.1/libdash.egg-info → libdash-0.5.3}/PKG-INFO +27 -3
  3. {libdash-0.5.1 → libdash-0.5.3}/README.md +26 -2
  4. libdash-0.5.3/libdash/__init__.py +3 -0
  5. {libdash-0.5.1 → libdash-0.5.3}/libdash/_dash.py +140 -8
  6. {libdash-0.5.1 → libdash-0.5.3}/libdash/ast.py +50 -23
  7. {libdash-0.5.1 → libdash-0.5.3}/libdash/parser.py +5 -0
  8. {libdash-0.5.1 → libdash-0.5.3/libdash.egg-info}/PKG-INFO +27 -3
  9. {libdash-0.5.1 → libdash-0.5.3}/libdash.egg-info/SOURCES.txt +1 -0
  10. {libdash-0.5.1 → libdash-0.5.3}/pyproject.toml +1 -1
  11. libdash-0.5.3/setup.py +146 -0
  12. {libdash-0.5.1 → libdash-0.5.3}/src/Makefile.am +20 -2
  13. {libdash-0.5.1 → libdash-0.5.3}/src/alias.c +44 -0
  14. {libdash-0.5.1 → libdash-0.5.3}/src/alias.h +4 -0
  15. {libdash-0.5.1 → libdash-0.5.3}/src/input.c +26 -0
  16. {libdash-0.5.1 → libdash-0.5.3}/src/input.h +1 -0
  17. libdash-0.5.3/src/mklayout +94 -0
  18. {libdash-0.5.1 → libdash-0.5.3}/src/parser.c +17 -5
  19. libdash-0.5.1/libdash/__init__.py +0 -2
  20. libdash-0.5.1/setup.py +0 -68
  21. {libdash-0.5.1 → libdash-0.5.3}/COPYING +0 -0
  22. {libdash-0.5.1 → libdash-0.5.3}/Makefile.am +0 -0
  23. {libdash-0.5.1 → libdash-0.5.3}/autogen.sh +0 -0
  24. {libdash-0.5.1 → libdash-0.5.3}/configure.ac +0 -0
  25. {libdash-0.5.1 → libdash-0.5.3}/libdash/printer.py +0 -0
  26. {libdash-0.5.1 → libdash-0.5.3}/libdash.egg-info/dependency_links.txt +0 -0
  27. {libdash-0.5.1 → libdash-0.5.3}/libdash.egg-info/top_level.txt +0 -0
  28. {libdash-0.5.1 → libdash-0.5.3}/setup.cfg +0 -0
  29. {libdash-0.5.1 → libdash-0.5.3}/src/TOUR +0 -0
  30. {libdash-0.5.1 → libdash-0.5.3}/src/arith_yacc.c +0 -0
  31. {libdash-0.5.1 → libdash-0.5.3}/src/arith_yacc.h +0 -0
  32. {libdash-0.5.1 → libdash-0.5.3}/src/arith_yylex.c +0 -0
  33. {libdash-0.5.1 → libdash-0.5.3}/src/bltin/bltin.h +0 -0
  34. {libdash-0.5.1 → libdash-0.5.3}/src/bltin/echo.1 +0 -0
  35. {libdash-0.5.1 → libdash-0.5.3}/src/bltin/printf.1 +0 -0
  36. {libdash-0.5.1 → libdash-0.5.3}/src/bltin/printf.c +0 -0
  37. {libdash-0.5.1 → libdash-0.5.3}/src/bltin/test.1 +0 -0
  38. {libdash-0.5.1 → libdash-0.5.3}/src/bltin/test.c +0 -0
  39. {libdash-0.5.1 → libdash-0.5.3}/src/bltin/times.c +0 -0
  40. {libdash-0.5.1 → libdash-0.5.3}/src/builtins.def.in +0 -0
  41. {libdash-0.5.1 → libdash-0.5.3}/src/cd.c +0 -0
  42. {libdash-0.5.1 → libdash-0.5.3}/src/cd.h +0 -0
  43. {libdash-0.5.1 → libdash-0.5.3}/src/dash.1 +0 -0
  44. {libdash-0.5.1 → libdash-0.5.3}/src/error.c +0 -0
  45. {libdash-0.5.1 → libdash-0.5.3}/src/error.h +0 -0
  46. {libdash-0.5.1 → libdash-0.5.3}/src/eval.c +0 -0
  47. {libdash-0.5.1 → libdash-0.5.3}/src/eval.h +0 -0
  48. {libdash-0.5.1 → libdash-0.5.3}/src/exec.c +0 -0
  49. {libdash-0.5.1 → libdash-0.5.3}/src/exec.h +0 -0
  50. {libdash-0.5.1 → libdash-0.5.3}/src/expand.c +0 -0
  51. {libdash-0.5.1 → libdash-0.5.3}/src/expand.h +0 -0
  52. {libdash-0.5.1 → libdash-0.5.3}/src/funcs/cmv +0 -0
  53. {libdash-0.5.1 → libdash-0.5.3}/src/funcs/dirs +0 -0
  54. {libdash-0.5.1 → libdash-0.5.3}/src/funcs/kill +0 -0
  55. {libdash-0.5.1 → libdash-0.5.3}/src/funcs/login +0 -0
  56. {libdash-0.5.1 → libdash-0.5.3}/src/funcs/newgrp +0 -0
  57. {libdash-0.5.1 → libdash-0.5.3}/src/funcs/popd +0 -0
  58. {libdash-0.5.1 → libdash-0.5.3}/src/funcs/pushd +0 -0
  59. {libdash-0.5.1 → libdash-0.5.3}/src/funcs/suspend +0 -0
  60. {libdash-0.5.1 → libdash-0.5.3}/src/histedit.c +0 -0
  61. {libdash-0.5.1 → libdash-0.5.3}/src/init.h +0 -0
  62. {libdash-0.5.1 → libdash-0.5.3}/src/jobs.c +0 -0
  63. {libdash-0.5.1 → libdash-0.5.3}/src/jobs.h +0 -0
  64. {libdash-0.5.1 → libdash-0.5.3}/src/machdep.h +0 -0
  65. {libdash-0.5.1 → libdash-0.5.3}/src/mail.c +0 -0
  66. {libdash-0.5.1 → libdash-0.5.3}/src/mail.h +0 -0
  67. {libdash-0.5.1 → libdash-0.5.3}/src/main.c +0 -0
  68. {libdash-0.5.1 → libdash-0.5.3}/src/main.h +0 -0
  69. {libdash-0.5.1 → libdash-0.5.3}/src/memalloc.c +0 -0
  70. {libdash-0.5.1 → libdash-0.5.3}/src/memalloc.h +0 -0
  71. {libdash-0.5.1 → libdash-0.5.3}/src/miscbltin.c +0 -0
  72. {libdash-0.5.1 → libdash-0.5.3}/src/miscbltin.h +0 -0
  73. {libdash-0.5.1 → libdash-0.5.3}/src/mkbuiltins +0 -0
  74. {libdash-0.5.1 → libdash-0.5.3}/src/mkinit.c +0 -0
  75. {libdash-0.5.1 → libdash-0.5.3}/src/mknodes.c +0 -0
  76. {libdash-0.5.1 → libdash-0.5.3}/src/mksignames.c +0 -0
  77. {libdash-0.5.1 → libdash-0.5.3}/src/mksyntax.c +0 -0
  78. {libdash-0.5.1 → libdash-0.5.3}/src/mktokens +0 -0
  79. {libdash-0.5.1 → libdash-0.5.3}/src/myhistedit.h +0 -0
  80. {libdash-0.5.1 → libdash-0.5.3}/src/mystring.c +0 -0
  81. {libdash-0.5.1 → libdash-0.5.3}/src/mystring.h +0 -0
  82. {libdash-0.5.1 → libdash-0.5.3}/src/nodes.c.pat +0 -0
  83. {libdash-0.5.1 → libdash-0.5.3}/src/nodetypes +0 -0
  84. {libdash-0.5.1 → libdash-0.5.3}/src/options.c +0 -0
  85. {libdash-0.5.1 → libdash-0.5.3}/src/options.h +0 -0
  86. {libdash-0.5.1 → libdash-0.5.3}/src/output.c +0 -0
  87. {libdash-0.5.1 → libdash-0.5.3}/src/output.h +0 -0
  88. {libdash-0.5.1 → libdash-0.5.3}/src/parser.h +0 -0
  89. {libdash-0.5.1 → libdash-0.5.3}/src/redir.c +0 -0
  90. {libdash-0.5.1 → libdash-0.5.3}/src/redir.h +0 -0
  91. {libdash-0.5.1 → libdash-0.5.3}/src/shell.h +0 -0
  92. {libdash-0.5.1 → libdash-0.5.3}/src/show.c +0 -0
  93. {libdash-0.5.1 → libdash-0.5.3}/src/show.h +0 -0
  94. {libdash-0.5.1 → libdash-0.5.3}/src/system.c +0 -0
  95. {libdash-0.5.1 → libdash-0.5.3}/src/system.h +0 -0
  96. {libdash-0.5.1 → libdash-0.5.3}/src/trap.c +0 -0
  97. {libdash-0.5.1 → libdash-0.5.3}/src/trap.h +0 -0
  98. {libdash-0.5.1 → libdash-0.5.3}/src/var.c +0 -0
  99. {libdash-0.5.1 → libdash-0.5.3}/src/var.h +0 -0
@@ -5,7 +5,7 @@ exclude src/dash
5
5
  exclude src/init.c src/mkinit
6
6
  exclude src/mknodes src/mksignames src/mksyntax
7
7
  exclude src/nodes.c src/nodes.h
8
- exclude src/signames.c
8
+ exclude src/signames.c src/libdash_layout.c
9
9
  exclude src/syntax.c src/syntax.h
10
10
  exclude src/token.h
11
11
  prune src/.deps
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libdash
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: Bindings for the dash shell as a library
5
5
  Author: PaSh contributors
6
6
  Author-email: Michael Greenberg <michael@greenberg.science>
@@ -124,9 +124,33 @@ For Python, see [`python/rt.py`](https://github.com/mgree/libdash/blob/master/py
124
124
 
125
125
  For OCaml, see [`ocaml/shell_to_json.ml`](https://github.com/mgree/libdash/blob/master/ocaml/shell_to_json.ml), a tool that parses shell syntax and produces JSON (using the [atdgen](https://opam.ocaml.org/packages/atdgen/) bindings).
126
126
 
127
- The ideal low-level interface to use is `parsecmd_safe` in `parser.c`; you'll need to ensure that dash's initialization routines have been called and that the stack marks are managed correctly. Parsing the POSIX shell is a complicated affair: beyond the usual locale issues, aliases affect the lexer, so one must use `setalias` and `unalias` to manage any aliases that ought to exist.
127
+ The ideal low-level interface to use is `parsecmd_safe` in `parser.c`; you'll need to ensure that dash's initialization routines have been called and that the stack marks are managed correctly. Parsing the POSIX shell is a complicated affair: beyond the usual locale issues, aliases affect the lexer, so one must use `setalias_safe` and `unalias` to manage any aliases that ought to exist. Outside of `parsecmd_safe`, call only entry points that cannot raise a dash error (e.g., `setinputfile_safe` rather than `setinputfile`).
128
128
 
129
- # How work with the parsed nodes
129
+ # Locales and multibyte characters
130
+
131
+ dash's lexer decodes multibyte characters using the process's current
132
+ `LC_CTYPE`. In a multibyte locale (e.g., `C.UTF-8`), each multibyte character
133
+ is wrapped in `CTLMBCHAR` markers, and the bindings decode it to Unicode.
134
+ In the `C` locale, it stays as raw bytes, and any byte that collides with a
135
+ dash control code is escaped with `CTLESC`. The same script can therefore
136
+ parse to different ASTs depending on the locale in effect when you parse.
137
+
138
+ libdash never sets the locale for you:
139
+
140
+ - The OCaml binding's `Dash.initialize` calls `initialize_dash_setlocale`,
141
+ i.e., `setlocale(LC_ALL, "")`, so it uses the environment's locale.
142
+ - The Python binding uses whatever locale the interpreter set at startup.
143
+ Python calls `setlocale(LC_CTYPE, "")` and coerces a `C` locale to
144
+ `C.UTF-8` (PEP 538), so Python usually parses in UTF-8 even when the
145
+ environment says `C`.
146
+ - Programs calling the C library directly (e.g., from C or another
147
+ language's FFI) stay in the `C` locale unless they call `setlocale` or
148
+ `initialize_dash_setlocale` themselves.
149
+
150
+ To get the same ASTs from different bindings, set the same `LC_CTYPE` in
151
+ each process before parsing.
152
+
153
+ # How to work with parsed nodes
130
154
 
131
155
  The general AST is described in `nodes.h`. There are some tricky invariants around the precise formatting of control codes; the OCaml code shows some examples of working with the `args` fields in `ocaml/ast.ml`, which converts the C AST to an OCaml AST.
132
156
 
@@ -52,9 +52,33 @@ For Python, see [`python/rt.py`](https://github.com/mgree/libdash/blob/master/py
52
52
 
53
53
  For OCaml, see [`ocaml/shell_to_json.ml`](https://github.com/mgree/libdash/blob/master/ocaml/shell_to_json.ml), a tool that parses shell syntax and produces JSON (using the [atdgen](https://opam.ocaml.org/packages/atdgen/) bindings).
54
54
 
55
- The ideal low-level interface to use is `parsecmd_safe` in `parser.c`; you'll need to ensure that dash's initialization routines have been called and that the stack marks are managed correctly. Parsing the POSIX shell is a complicated affair: beyond the usual locale issues, aliases affect the lexer, so one must use `setalias` and `unalias` to manage any aliases that ought to exist.
55
+ The ideal low-level interface to use is `parsecmd_safe` in `parser.c`; you'll need to ensure that dash's initialization routines have been called and that the stack marks are managed correctly. Parsing the POSIX shell is a complicated affair: beyond the usual locale issues, aliases affect the lexer, so one must use `setalias_safe` and `unalias` to manage any aliases that ought to exist. Outside of `parsecmd_safe`, call only entry points that cannot raise a dash error (e.g., `setinputfile_safe` rather than `setinputfile`).
56
56
 
57
- # How work with the parsed nodes
57
+ # Locales and multibyte characters
58
+
59
+ dash's lexer decodes multibyte characters using the process's current
60
+ `LC_CTYPE`. In a multibyte locale (e.g., `C.UTF-8`), each multibyte character
61
+ is wrapped in `CTLMBCHAR` markers, and the bindings decode it to Unicode.
62
+ In the `C` locale, it stays as raw bytes, and any byte that collides with a
63
+ dash control code is escaped with `CTLESC`. The same script can therefore
64
+ parse to different ASTs depending on the locale in effect when you parse.
65
+
66
+ libdash never sets the locale for you:
67
+
68
+ - The OCaml binding's `Dash.initialize` calls `initialize_dash_setlocale`,
69
+ i.e., `setlocale(LC_ALL, "")`, so it uses the environment's locale.
70
+ - The Python binding uses whatever locale the interpreter set at startup.
71
+ Python calls `setlocale(LC_CTYPE, "")` and coerces a `C` locale to
72
+ `C.UTF-8` (PEP 538), so Python usually parses in UTF-8 even when the
73
+ environment says `C`.
74
+ - Programs calling the C library directly (e.g., from C or another
75
+ language's FFI) stay in the `C` locale unless they call `setlocale` or
76
+ `initialize_dash_setlocale` themselves.
77
+
78
+ To get the same ASTs from different bindings, set the same `LC_CTYPE` in
79
+ each process before parsing.
80
+
81
+ # How to work with parsed nodes
58
82
 
59
83
  The general AST is described in `nodes.h`. There are some tricky invariants around the precise formatting of control codes; the OCaml code shows some examples of working with the `args` fields in `ocaml/ast.ml`, which converts the C AST to an OCaml AST.
60
84
 
@@ -0,0 +1,3 @@
1
+ from .parser import parse, check_layout
2
+ from ._dash import LibdashLayoutError
3
+ from .printer import to_string
@@ -36,11 +36,12 @@ NNOT = 25;
36
36
  # size_t stacknleft;
37
37
  # };
38
38
  #
39
- # We only care about getting the struct size correct, not the contents.
39
+ # The bindings use it only as an opaque buffer of this size; the fields
40
+ # carry their C names so that the layout check covers them too.
40
41
  class stackmark (Structure):
41
- _fields_ = [("stackp", c_void_p),
42
- ("nxt", c_void_p),
43
- ("size", c_size_t)];
42
+ _fields_ = [("stackp", c_void_p),
43
+ ("stacknxt", c_void_p),
44
+ ("stacknleft", c_size_t)];
44
45
 
45
46
  def init_stack (libdash):
46
47
  stack = create_string_buffer (sizeof (stackmark));
@@ -75,6 +76,7 @@ def initialize_dash_errno (libdash):
75
76
 
76
77
 
77
78
  def initialize (libdash):
79
+ require_layout (libdash);
78
80
  initialize_dash_errno (libdash);
79
81
  dash_init (libdash);
80
82
 
@@ -88,9 +90,24 @@ def setinputtostdin (libdash):
88
90
 
89
91
  # TODO: allow push parameter
90
92
  def setinputfile (libdash, filename):
91
- libdash.setinputfile.argtypes = [c_char_p, c_int];
92
- libdash.setinputfile.restypes = c_int;
93
- libdash.setinputfile (filename.encode ('utf-8'), 0);
93
+ libdash.setinputfile_safe.argtypes = [c_char_p, c_int];
94
+ libdash.setinputfile_safe.restype = c_int;
95
+ if (libdash.setinputfile_safe (filename.encode ('utf-8'), 0) < 0):
96
+ raise OSError ("cannot open %s" % filename);
97
+
98
+
99
+ def setalias (libdash, name, value):
100
+ libdash.setalias_safe.argtypes = [c_char_p, c_char_p];
101
+ libdash.setalias_safe.restype = c_int;
102
+ if (libdash.setalias_safe (name.encode ('utf-8'), value.encode ('utf-8')) != 0):
103
+ raise ValueError ("invalid alias name: %r" % name);
104
+
105
+
106
+ def unalias (libdash, name):
107
+ """Returns True if `name` was an alias."""
108
+ libdash.unalias.argtypes = [c_char_p];
109
+ libdash.unalias.restype = c_int;
110
+ return (libdash.unalias (name.encode ('utf-8')) == 0);
94
111
 
95
112
 
96
113
  def parsecmd_safe (libdash, interactive):
@@ -261,6 +278,8 @@ class parsefile (Structure):
261
278
  # /* Number of outstanding calls to pungetc. */
262
279
  # int unget;
263
280
  # };
281
+ #
282
+ # No lleft: configure.ac defines SMALL unless dash is built --with-libedit.
264
283
  parsefile._fields_ = [("prev", POINTER (parsefile)),
265
284
  ("linno", c_int),
266
285
  ("fd", c_int),
@@ -271,10 +290,123 @@ parsefile._fields_ = [("prev", POINTER (parsefile)),
271
290
  ("strpush", POINTER (strpush)),
272
291
  ("basestrpush", strpush),
273
292
  ("spfree", POINTER (strpush)),
274
- ("lleft", c_int),
275
293
  ("unget", c_int)];
276
294
 
277
295
 
296
+ # Every struct above, by its C name. libdash.so's libdash_struct_layout
297
+ # (generated by src/mklayout from the same list in src/Makefile.am) holds
298
+ # their real sizes and field offsets; require_layout compares the two.
299
+ LAYOUT = [("struct stackmark", stackmark),
300
+ ("struct nodelist", nodelist),
301
+ ("union node", union_node),
302
+ ("struct ncmd", ncmd),
303
+ ("struct npipe", npipe),
304
+ ("struct nredir", nredir),
305
+ ("struct nbinary", nbinary),
306
+ ("struct nif", nif),
307
+ ("struct nfor", nfor),
308
+ ("struct ncase", ncase),
309
+ ("struct nclist", nclist),
310
+ ("struct ndefun", ndefun),
311
+ ("struct narg", narg),
312
+ ("struct nfile", nfile),
313
+ ("struct ndup", ndup),
314
+ ("struct nhere", nhere),
315
+ ("struct nnot", nnot),
316
+ ("struct strpush", strpush),
317
+ ("struct parsefile", parsefile)];
318
+
319
+
320
+ class LibdashLayoutError (Exception):
321
+ """The ctypes structs in libdash/_dash.py disagree with the ones
322
+ libdash.so was built with, so reading the AST would read the wrong
323
+ fields."""
324
+ pass;
325
+
326
+
327
+ class _layout_row (Structure):
328
+ _fields_ = [("type", c_char_p),
329
+ ("field", c_char_p), # NULL: value is the sizeof type
330
+ ("value", c_size_t)];
331
+
332
+
333
+ def library_layout (libdash):
334
+ """libdash.so's layout table, as {C type: (sizeof, {field: offset})},
335
+ with fields in declaration order."""
336
+ first = _layout_row.in_dll (libdash, "libdash_struct_layout");
337
+ rows = cast (addressof (first), POINTER (_layout_row));
338
+
339
+ out = {};
340
+ i = 0;
341
+ while (rows [i].type is not None):
342
+ row = rows [i];
343
+ ctype = row.type.decode ();
344
+ if (row.field is None):
345
+ out [ctype] = (row.value, {});
346
+ else:
347
+ out [ctype][1][row.field.decode ()] = row.value;
348
+ i += 1;
349
+
350
+ return out;
351
+
352
+
353
+ def layout_problems (libdash, layout):
354
+ """How `layout` (rows like LAYOUT's) disagrees with libdash.so's table,
355
+ as a list of messages; empty if they agree."""
356
+ c = library_layout (libdash);
357
+ mine = dict (layout);
358
+ problems = [];
359
+
360
+ for ctype in c:
361
+ if ctype not in mine:
362
+ problems.append ("%s: in libdash.so, but has no ctypes mirror" % ctype);
363
+
364
+ for (ctype, cls) in layout:
365
+ if ctype not in c:
366
+ problems.append ("%s: has a ctypes mirror, but is not in libdash.so" % ctype);
367
+ continue;
368
+
369
+ (size, offsets) = c [ctype];
370
+ fields = [name for (name, _t) in cls._fields_];
371
+
372
+ if (sizeof (cls) != size):
373
+ problems.append ("%s: sizeof is %d in C, %d in ctypes"
374
+ % (ctype, size, sizeof (cls)));
375
+
376
+ for (name, offset) in offsets.items ():
377
+ if name not in fields:
378
+ problems.append ("%s.%s: in C (offset %d), missing from ctypes"
379
+ % (ctype, name, offset));
380
+ elif (getattr (cls, name).offset != offset):
381
+ problems.append ("%s.%s: offset is %d in C, %d in ctypes"
382
+ % (ctype, name, offset, getattr (cls, name).offset));
383
+
384
+ for name in fields:
385
+ if name not in offsets:
386
+ problems.append ("%s.%s: in ctypes, not in C" % (ctype, name));
387
+
388
+ return problems;
389
+
390
+
391
+ _layout_checked = False;
392
+
393
+ def require_layout (libdash, force = False):
394
+ """Raises LibdashLayoutError unless LAYOUT matches libdash.so. Checks
395
+ once per process, unless `force`."""
396
+ global _layout_checked;
397
+
398
+ if (_layout_checked and not force):
399
+ return;
400
+
401
+ problems = layout_problems (libdash, LAYOUT);
402
+ if problems:
403
+ raise LibdashLayoutError (
404
+ "libdash/_dash.py's ctypes structs disagree with libdash.so:\n "
405
+ + "\n ".join (problems));
406
+
407
+ _layout_checked = True;
408
+
409
+
278
410
  # dash.ast
279
411
  # let rec nodelist (n : nodelist structure ptr) : (node union ptr) list =
280
412
  # if nullptr n
@@ -230,12 +230,14 @@ def of_binary (n):
230
230
  return [of_node (n.nbinary.ch1), of_node (n.nbinary.ch2)]
231
231
 
232
232
 
233
- def to_arg (narg):
233
+ def to_arg (narg, assign = False):
234
234
  s = explode_rev (narg.text)
235
235
  bqlist = narg.backquote
236
236
  stack = []
237
237
 
238
- a = parse_arg (s, bqlist, stack)
238
+ # An assignment word starts with its name; tilde prefixes may only come
239
+ # after the first '=' (and after unquoted ':'s).
240
+ a = parse_arg (s, bqlist, stack, assign = assign, tilde_ok = not assign)
239
241
 
240
242
  assert (len (s) == 0)
241
243
  # assert (nullptr bqlist)
@@ -248,10 +250,26 @@ def to_arg (narg):
248
250
  return (a)
249
251
 
250
252
 
251
- def parse_arg (s, bqlist, stack):
253
+ def parse_arg (s, bqlist, stack, assign = False, tilde_ok = False):
254
+ """`tilde_ok`: a tilde prefix may start here (the start of a word). In an
255
+ assignment (`assign`), one may also start right after the first '=' and
256
+ after any unquoted ':'."""
252
257
  acc = []
258
+ seen_len = 0
259
+ seen_equals = False
253
260
 
254
261
  while (True):
262
+ # after each new element, decide whether a tilde prefix may follow it
263
+ if (len (acc) != seen_len):
264
+ seen_len = len (acc)
265
+ last = acc [-1]
266
+
267
+ if (assign and not seen_equals and last == ["C", ORD_EQUALS]):
268
+ seen_equals = True
269
+ tilde_ok = True
270
+ else:
271
+ tilde_ok = assign and (last == ["C", ORD_COLON])
272
+
255
273
  s_len = len (s)
256
274
  # stack_len = len (stack)
257
275
 
@@ -324,7 +342,7 @@ def parse_arg (s, bqlist, stack):
324
342
 
325
343
  stack.append (STACK_CTLVAR)
326
344
 
327
- a = parse_arg (s, bqlist, stack)
345
+ a = parse_arg (s, bqlist, stack, assign = assign, tilde_ok = True)
328
346
 
329
347
  v = ["V", [var_type (vstype), (t & 0x10 == 0x10), var_name, a]]
330
348
  elif (len (s) >= 1):
@@ -424,7 +442,7 @@ def parse_arg (s, bqlist, stack):
424
442
 
425
443
  stack.append (STACK_CTLARI)
426
444
 
427
- a = parse_arg (s, bqlist, stack)
445
+ a = parse_arg (s, bqlist, stack, assign = assign)
428
446
 
429
447
  # TODO: assert (stack = stack')
430
448
 
@@ -465,7 +483,7 @@ def parse_arg (s, bqlist, stack):
465
483
  s.pop ()
466
484
  stack.append (STACK_CTLQUO)
467
485
 
468
- a = parse_arg (s, bqlist, stack)
486
+ a = parse_arg (s, bqlist, stack, assign = assign)
469
487
 
470
488
  acc.append (["Q", a])
471
489
 
@@ -474,12 +492,17 @@ def parse_arg (s, bqlist, stack):
474
492
  elif (s [-1] == ORD_TILDE):
475
493
  s.pop ()
476
494
 
477
- if ((STACK_CTLQUO in stack) or (STACK_CTLARI in stack)):
495
+ # no tilde prefix here, in arithmetic, or in double quotes
496
+ if ((not tilde_ok) or (STACK_CTLQUO in stack) or (STACK_CTLARI in stack)):
478
497
  acc.append (["C", ORD_TILDE])
479
498
  else:
480
- uname = parse_tilde (s)
499
+ (is_prefix, uname) = parse_tilde (s, assign)
481
500
 
482
- acc.append (["T", uname])
501
+ if (is_prefix):
502
+ acc.append (["T", uname])
503
+ else:
504
+ # interrupted: the ~ and what follows are ordinary
505
+ acc.append (["C", ORD_TILDE])
483
506
 
484
507
  # (* ordinary character *)
485
508
  # | c::s,_ -> arg_char (C c) s bqlist stack
@@ -496,22 +519,26 @@ def stringOrNull (acc_str):
496
519
  return ["Some", acc_str]
497
520
 
498
521
 
499
- def parse_tilde (s):
500
- acc_str = ""
522
+ def parse_tilde (s, assign):
523
+ """Returns (True, uname) and consumes the tilde prefix at the end of the
524
+ reversed `s` (uname is "None" for the bare ~), or (False, None), leaving
525
+ `s` untouched, if there is no tilde prefix (see exptilde in
526
+ src/expand.c)."""
527
+ i = len (s)
501
528
 
502
529
  while (True):
503
- if (s == []):
504
- return stringOrNull (acc_str)
505
- else:
506
- s_last = s [-1]
530
+ i = i - 1
507
531
 
508
- if (s_last in [CTLESC, CTLVAR, CTLQUOTEMARK, CTLBACKQ, CTLARI]):
509
- return ("None")
510
- elif (s_last in [CTLENDVAR, CTLENDARI, ORD_COLON, ORD_SLASH]):
511
- return (stringOrNull (acc_str))
512
- else:
513
- c = s.pop ()
514
- acc_str = acc_str + chr (c)
532
+ if ((i < 0) or (s [i] in [CTLENDVAR, CTLENDARI, ORD_SLASH])
533
+ or (assign and (s [i] == ORD_COLON))):
534
+ break
535
+ elif (s [i] in [CTLESC, CTLVAR, CTLQUOTEMARK, CTLBACKQ, CTLARI]):
536
+ return (False, None)
537
+
538
+ acc_str = "".join (chr (c) for c in reversed (s [i + 1:]))
539
+ del s [i + 1:]
540
+
541
+ return (True, stringOrNull (acc_str))
515
542
 
516
543
 
517
544
  def to_assign (a_rev):
@@ -551,7 +578,7 @@ def to_assigns (n):
551
578
  sys.stdout.flush ()
552
579
  os.abort ()
553
580
 
554
- arg = to_arg (n.contents.narg)
581
+ arg = to_arg (n.contents.narg, assign = True)
555
582
 
556
583
  arg.reverse ()
557
584
  assigns.append (to_assign (arg))
@@ -16,6 +16,11 @@ def libdash_library_path():
16
16
  LIBDASH_LIBRARY_PATH = os.path.join(FILE_PATH, "libdash.so")
17
17
  return LIBDASH_LIBRARY_PATH
18
18
 
19
+ def check_layout():
20
+ """Raises LibdashLayoutError if the bindings' ctypes structs disagree
21
+ with libdash.so's. parse() checks this itself before its first parse."""
22
+ require_layout(CDLL(libdash_library_path()), force=True)
23
+
19
24
  # parsefile->eof bit 1: set by preadbuffer at EOF, never cleared by pungetc.
20
25
  PARSEFILE_EOF = 2;
21
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: libdash
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: Bindings for the dash shell as a library
5
5
  Author: PaSh contributors
6
6
  Author-email: Michael Greenberg <michael@greenberg.science>
@@ -124,9 +124,33 @@ For Python, see [`python/rt.py`](https://github.com/mgree/libdash/blob/master/py
124
124
 
125
125
  For OCaml, see [`ocaml/shell_to_json.ml`](https://github.com/mgree/libdash/blob/master/ocaml/shell_to_json.ml), a tool that parses shell syntax and produces JSON (using the [atdgen](https://opam.ocaml.org/packages/atdgen/) bindings).
126
126
 
127
- The ideal low-level interface to use is `parsecmd_safe` in `parser.c`; you'll need to ensure that dash's initialization routines have been called and that the stack marks are managed correctly. Parsing the POSIX shell is a complicated affair: beyond the usual locale issues, aliases affect the lexer, so one must use `setalias` and `unalias` to manage any aliases that ought to exist.
127
+ The ideal low-level interface to use is `parsecmd_safe` in `parser.c`; you'll need to ensure that dash's initialization routines have been called and that the stack marks are managed correctly. Parsing the POSIX shell is a complicated affair: beyond the usual locale issues, aliases affect the lexer, so one must use `setalias_safe` and `unalias` to manage any aliases that ought to exist. Outside of `parsecmd_safe`, call only entry points that cannot raise a dash error (e.g., `setinputfile_safe` rather than `setinputfile`).
128
128
 
129
- # How work with the parsed nodes
129
+ # Locales and multibyte characters
130
+
131
+ dash's lexer decodes multibyte characters using the process's current
132
+ `LC_CTYPE`. In a multibyte locale (e.g., `C.UTF-8`), each multibyte character
133
+ is wrapped in `CTLMBCHAR` markers, and the bindings decode it to Unicode.
134
+ In the `C` locale, it stays as raw bytes, and any byte that collides with a
135
+ dash control code is escaped with `CTLESC`. The same script can therefore
136
+ parse to different ASTs depending on the locale in effect when you parse.
137
+
138
+ libdash never sets the locale for you:
139
+
140
+ - The OCaml binding's `Dash.initialize` calls `initialize_dash_setlocale`,
141
+ i.e., `setlocale(LC_ALL, "")`, so it uses the environment's locale.
142
+ - The Python binding uses whatever locale the interpreter set at startup.
143
+ Python calls `setlocale(LC_CTYPE, "")` and coerces a `C` locale to
144
+ `C.UTF-8` (PEP 538), so Python usually parses in UTF-8 even when the
145
+ environment says `C`.
146
+ - Programs calling the C library directly (e.g., from C or another
147
+ language's FFI) stay in the `C` locale unless they call `setlocale` or
148
+ `initialize_dash_setlocale` themselves.
149
+
150
+ To get the same ASTs from different bindings, set the same `LC_CTYPE` in
151
+ each process before parsing.
152
+
153
+ # How to work with parsed nodes
130
154
 
131
155
  The general AST is described in `nodes.h`. There are some tricky invariants around the precise formatting of control codes; the OCaml code shows some examples of working with the `args` fields in `ocaml/ast.ml`, which converts the C AST to an OCaml AST.
132
156
 
@@ -51,6 +51,7 @@ src/miscbltin.c
51
51
  src/miscbltin.h
52
52
  src/mkbuiltins
53
53
  src/mkinit.c
54
+ src/mklayout
54
55
  src/mknodes.c
55
56
  src/mksignames.c
56
57
  src/mksyntax.c
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "libdash"
3
- version = "0.5.1"
3
+ version = "0.5.3"
4
4
  authors = [
5
5
  { name="Michael Greenberg", email="michael@greenberg.science" },
6
6
  { name="PaSh contributors" },
libdash-0.5.3/setup.py ADDED
@@ -0,0 +1,146 @@
1
+ from setuptools import Extension, setup
2
+ from setuptools.command.build_ext import build_ext
3
+
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+
10
+ from pathlib import Path
11
+
12
+ HERE = Path(__file__).parent.resolve()
13
+ long_description = (HERE / "README.md").read_text()
14
+
15
+ # What autotools needs to build the library.
16
+ SOURCES = ['configure.ac', 'Makefile.am', 'src']
17
+
18
+
19
+ def git_lines(*args):
20
+ """The NUL-separated output of `git args` (which must include -z) in
21
+ HERE, or None if HERE isn't the top of a git work tree (e.g., an
22
+ unpacked sdist)."""
23
+ try:
24
+ top = subprocess.run(['git', 'rev-parse', '--show-toplevel'], cwd=HERE,
25
+ capture_output=True, text=True)
26
+ if top.returncode != 0 or Path(top.stdout.strip()).resolve() != HERE:
27
+ return None
28
+ out = subprocess.run(['git', *args], cwd=HERE,
29
+ capture_output=True, text=True, check=True)
30
+ except (OSError, subprocess.CalledProcessError):
31
+ return None
32
+ return [line for line in out.stdout.split('\0') if line]
33
+
34
+
35
+ def source_files():
36
+ """The files under SOURCES, relative to HERE.
37
+
38
+ In a git checkout, exactly the tracked files, so that leftovers from an
39
+ in-tree build (e.g., a stale src/builtins.h) can never reach the build.
40
+ In an sdist, everything: MANIFEST.in chose what to pack."""
41
+ tracked = git_lines('ls-files', '-z', '--', *SOURCES)
42
+ if tracked is None:
43
+ return [str(f.relative_to(HERE))
44
+ for d in SOURCES
45
+ for f in ([HERE / d] if (HERE / d).is_file() else (HERE / d).rglob('*'))
46
+ if f.is_file()]
47
+
48
+ leftovers = git_lines('ls-files', '-z', '--others', '--', *SOURCES)
49
+ if leftovers:
50
+ print(f'warning: ignoring {len(leftovers)} untracked file(s) in the source tree, '
51
+ 'e.g., from an in-tree build (`git clean -fdX src` removes them):',
52
+ file=sys.stderr)
53
+ for f in leftovers[:10]:
54
+ print(f' {f}', file=sys.stderr)
55
+ if len(leftovers) > 10:
56
+ print(f' ... and {len(leftovers) - 10} more', file=sys.stderr)
57
+ return tracked
58
+
59
+
60
+ def try_exec(*cmds, cwd):
61
+ proc = subprocess.run(cmds, cwd=cwd)
62
+
63
+ if proc.returncode != 0:
64
+ print('`{}` failed'.format(' '.join(cmds)), file=sys.stderr)
65
+ proc.check_returncode()
66
+
67
+
68
+ class AutotoolsLibrary(Extension):
69
+ """libdash's shared library, built by autotools and loaded with ctypes
70
+ (it is not a Python extension module)."""
71
+
72
+ def __init__(self, name):
73
+ super().__init__(name, sources=[])
74
+
75
+
76
+ class libdash_build_ext(build_ext):
77
+ """Builds the library in a copy of the C sources under build_temp, so that
78
+ autoreconf, configure, and make never write into the source tree."""
79
+
80
+ def run(self):
81
+ self.build_autotools()
82
+
83
+ def package_dir(self):
84
+ # in place (e.g., editable installs): next to the Python sources
85
+ return HERE / 'libdash' if self.inplace else Path(self.build_lib) / 'libdash'
86
+
87
+ def library_names(self):
88
+ names = ['libdash.so']
89
+ if sys.platform == 'darwin':
90
+ names.append('libdash.dylib')
91
+ return names
92
+
93
+ def get_outputs(self):
94
+ return [str(self.package_dir() / name) for name in self.library_names()]
95
+
96
+ def get_output_mapping(self):
97
+ return {}
98
+
99
+ def build_autotools(self):
100
+ if sys.platform == 'darwin':
101
+ target_arch = os.environ.get("ARCHFLAGS")
102
+ build_arch = platform.machine()
103
+ if not target_arch:
104
+ target_arch = f"-arch {build_arch}"
105
+ os.environ["ARCHFLAGS"] = target_arch
106
+ if build_arch not in target_arch and "MACOSX_DEPLOYMENT_TARGET" not in os.environ:
107
+ os.environ["MACOSX_DEPLOYMENT_TARGET"] = "11.0"
108
+
109
+ # configure reads CFLAGS/LDFLAGS, not CCFLAGS; an explicit CFLAGS
110
+ # replaces autoconf's default -g -O2, so keep it
111
+ os.environ["CFLAGS"] = f'{os.environ.get("CFLAGS", "-g -O2")} {target_arch}'
112
+ os.environ["LDFLAGS"] = f'{os.environ.get("LDFLAGS", "")} {target_arch}'.strip()
113
+
114
+ print(f'ARCHFLAGS: {target_arch} MACOSX_DEPLOYMENT_TARGET: {os.environ.get("MACOSX_DEPLOYMENT_TARGET", "")} CFLAGS: {os.environ.get("CFLAGS")} LDFLAGS: {os.environ.get("LDFLAGS")}')
115
+
116
+ # A fresh copy every time: cibuildwheel builds every wheel from the
117
+ # same source tree, and objects from a previous build may target a
118
+ # different arch.
119
+ work = Path(self.build_temp).resolve() / 'autotools'
120
+ if work.exists():
121
+ shutil.rmtree(work)
122
+ work.mkdir(parents=True)
123
+
124
+ for f in source_files():
125
+ (work / f).parent.mkdir(parents=True, exist_ok=True)
126
+ shutil.copy2(HERE / f, work / f)
127
+
128
+ try_exec('autoreconf', '--install', '--force', cwd=work)
129
+ try_exec('./configure', cwd=work)
130
+ try_exec('make', cwd=work)
131
+
132
+ dest = self.package_dir()
133
+ dest.mkdir(parents=True, exist_ok=True)
134
+ shutil.copy2(work / 'src/.libs/dlldash.so', dest / 'libdash.so')
135
+ if sys.platform == 'darwin':
136
+ shutil.copy2(work / 'src/.libs/libdash.dylib', dest / 'libdash.dylib')
137
+
138
+
139
+ setup(name='libdash',
140
+ packages=['libdash'],
141
+ ext_modules=[AutotoolsLibrary('libdash.libdash')],
142
+ cmdclass={'build_ext': libdash_build_ext},
143
+ version='0.5.3',
144
+ long_description=long_description,
145
+ long_description_content_type='text/markdown',
146
+ include_package_data=True)