Skip to content

Commit debbeb6

Browse files
authored
Merge pull request #2410 from srcML/issue2311
Add line column and range position to input filenames
2 parents 00dca93 + c8c6870 commit debbeb6

9 files changed

Lines changed: 616 additions & 4 deletions

File tree

‎doc/manpage/srcml.cfg‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# SYNOPSIS
88

9-
`srcml` \[[general-options\]][GENERAL OPTIONS] \[[srcML-options\]][CREATING SRCML] \[[transformations\]][TRANSFORMATIONS] \[[output-src-options\]][EXTRACTING SOURCE CODE] \[input\] \[output\]
9+
`srcml` \[[general-options\]][GENERAL OPTIONS] \[[srcML-options\]][CREATING SRCML] \[[transformations\]][TRANSFORMATIONS] \[[output-src-options\]][EXTRACTING SOURCE CODE] \[input\[[:position\]][POSITION SUFFIX]\] \[output\]
1010

1111

1212
# DESCRIPTION
@@ -28,7 +28,8 @@ tolerant to incomplete source code and uncompilable code.
2828

2929
Use of the character '-' in the place of an input file, or providing no input
3030
file, implies reading from standard input. A source-code language must be specified
31-
when source-code input is from standard input.
31+
when source-code input is from standard input. An input filename may end with a
32+
position suffix to convert only part of a file, as described in [POSITION SUFFIX].
3233

3334
# GENERAL OPTIONS
3435

@@ -462,6 +463,24 @@ of each unit in the archive to standard out.
462463
$ srcml archive.xml --xpath "//src:unit/@filename"
463464
```
464465

466+
# POSITION SUFFIX
467+
468+
An input source-code filename may end with a position suffix, converting only that
469+
part of the file:
470+
471+
| Input | Converted |
472+
|-|-|
473+
| `main.cpp:191` | line 191 |
474+
| `main.cpp:191:5` | line 191 from column 5 |
475+
| `main.cpp:191-222` | lines 191 through 222 |
476+
| `main.cpp:191-` | line 191 to the end |
477+
| `main.cpp:-222` | the start through line 222 |
478+
| `main.cpp:191:5-222:12` | 191:5 through 222:12 |
479+
480+
Lines and columns start at one, and a position past an end is that end. A zero, or a
481+
range that ends before it starts, is an error. The suffix only applies when no file of
482+
that exact name exists, and is not allowed on a directory, an archive, or srcML input.
483+
465484
# EXIT STATUS
466485

467486
**${STATUS_SUCCESS}**: Normal

‎src/client/create_srcml.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ int srcml_handler_dispatch(ParseQueue& queue,
8484
return src_input_filesystem(queue, srcml_arch, srcml_request, input);
8585
}
8686

87-
if (input.protocol == "file"sv && !option(SRCML_COMMAND_HEADER) && input.archives.empty() && input.compressions.empty() && srcml_archive_check_extension(srcml_arch, input.extension.data())) {
87+
// a "filename:N" line suffix needs the content read into a buffer so it can be sliced,
88+
// so it skips the disk-filename fast path
89+
if (input.protocol == "file"sv && input.line == 0 && !option(SRCML_COMMAND_HEADER) && input.archives.empty() && input.compressions.empty() && srcml_archive_check_extension(srcml_arch, input.extension.data())) {
8890

8991
return src_input_file(queue, srcml_arch, srcml_request, input);
9092
}

‎src/client/src_input_libarchive.cpp‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,62 @@ int src_input_libarchive(ParseQueue& queue,
495495

496496
prequest->status = !prequest->language.empty() ? 0 : SRCML_STATUS_UNSET_LANGUAGE;
497497

498+
// a range suffix, e.g. "filename:191:5-222:12", keeps only that part of the read buffer
499+
if (input_file.line != 0) {
500+
auto& buf = prequest->buffer;
501+
502+
// offset of the start of a line, or the end of the buffer when the line is past the end of the file
503+
auto line_start = [&buf](int line) {
504+
505+
size_t offset = 0;
506+
for (int cur = 1; cur < line && offset < buf.size(); ++cur) {
507+
508+
auto nl = std::find(buf.begin() + (std::ptrdiff_t) offset, buf.end(), '\n');
509+
if (nl == buf.end())
510+
return buf.size();
511+
512+
offset = (size_t) (nl - buf.begin()) + 1;
513+
}
514+
515+
return offset;
516+
};
517+
518+
// offset of the end of the line at an offset, not including the line terminator
519+
auto line_end = [&buf](size_t offset) {
520+
return (size_t) (std::find(buf.begin() + (std::ptrdiff_t) offset, buf.end(), '\n') - buf.begin());
521+
};
522+
523+
size_t start = line_start(input_file.line);
524+
525+
// a line past the end of the file has no source
526+
if (start >= buf.size()) {
527+
buf.clear();
528+
529+
} else {
530+
531+
// a column starts the line at that position, clamped to the end of the line
532+
if (input_file.column != 0)
533+
start = std::min(start + (size_t) input_file.column - 1, line_end(start));
534+
535+
// an open range ends at the end of the file
536+
size_t stop = buf.size();
537+
if (input_file.end_line != srcml_input_src::END_OF_FILE) {
538+
539+
size_t end_start = line_start(input_file.end_line);
540+
size_t eol = line_end(end_start);
541+
542+
// an end column ends the range at that position, otherwise it is the entire line and its terminator
543+
stop = input_file.end_column != 0 ? std::min(end_start + (size_t) input_file.end_column, eol)
544+
: std::min(eol + 1, buf.size());
545+
}
546+
547+
std::vector<char> slice(buf.begin() + (std::ptrdiff_t) start, buf.begin() + (std::ptrdiff_t) std::max(start, stop));
548+
if (slice.empty() || slice.back() != '\n')
549+
slice.push_back('\n');
550+
buf.swap(slice);
551+
}
552+
}
553+
498554
queue.schedule(prequest);
499555

500556
++count;

‎src/client/srcml.cpp‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,47 @@ See `srcml --help` for more information.
151151
input.state = input.issrcML ? SRCML : SRC;
152152
}
153153

154+
// a line and column suffix is a position in a source file, and they start at one
155+
for (const auto& input : srcml_request.input_sources) {
156+
157+
if (input.line == srcml_input_src::INVALID_POSITION || input.end_line == srcml_input_src::INVALID_POSITION) {
158+
SRCMLstatus(ERROR_MSG, "srcml: Invalid line number 0 in %s", src_prefix_resource(input.filename));
159+
exit(1);
160+
}
161+
162+
if (input.column == srcml_input_src::INVALID_POSITION || input.end_column == srcml_input_src::INVALID_POSITION) {
163+
SRCMLstatus(ERROR_MSG, "srcml: Invalid column number 0 in %s", src_prefix_resource(input.filename));
164+
exit(1);
165+
}
166+
167+
if (input.line == 0)
168+
continue;
169+
170+
// the end of a range is at, or after, the start
171+
if (input.end_line != srcml_input_src::END_OF_FILE &&
172+
(input.end_line < input.line ||
173+
(input.end_line == input.line && input.end_column != 0 && input.end_column < std::max(input.column, 1)))) {
174+
SRCMLstatus(ERROR_MSG, "srcml: Invalid range with the end before the start in %s", src_prefix_resource(input.filename));
175+
exit(1);
176+
}
177+
178+
if (input.state == SRCML) {
179+
SRCMLstatus(ERROR_MSG, "srcml: Line suffix not allowed on the srcML input %s", src_prefix_resource(input.filename));
180+
exit(1);
181+
}
182+
183+
if (input.isdirectory) {
184+
SRCMLstatus(ERROR_MSG, "srcml: Line suffix not allowed on the directory %s", src_prefix_resource(input.filename));
185+
exit(1);
186+
}
187+
188+
// an archive holds more than one file, so a position in it is ambiguous, while a compression is a single file
189+
if (!input.archives.empty()) {
190+
SRCMLstatus(ERROR_MSG, "srcml: Line suffix not allowed on the archive %s", src_prefix_resource(input.filename));
191+
exit(1);
192+
}
193+
}
194+
154195
/*
155196
Setup the internal pipeline of possible steps:
156197
* creating srcml from src files and input srcml files, and transforming srcml

‎src/client/srcml_cli.cpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,17 @@ static std::string languageList() {
3535

3636
static std::string srcmlHeader() {
3737

38-
return R"(Usage: srcml [options] <src_infile>... [-o <srcML_outfile>]
38+
return R"(Usage: srcml [options] <src_infile>[:<position>]... [-o <srcML_outfile>]
3939
srcml [options] <srcML_infile>... [-o <src_outfile>]
4040
4141
Translates )" + languageList() + R"( source code to and from the XML source-code representation srcML. Also supports querying and transformation of srcML.
4242
4343
Source-code input can be from text, the system clipboard, standard input,
4444
a file, a directory, or an archive file, i.e., tar, cpio, and zip. Multiple
4545
files are stored in a srcML archive.
46+
47+
A source-code filename may end with a position, e.g., main.cpp:191 or
48+
main.cpp:191-222, to convert only those lines of the file.
4649
)";
4750
}
4851

‎src/client/srcml_input_src.cpp‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
#include <srcml_input_src.hpp>
11+
#include <input_curl.hpp>
1112
#include <string_view>
1213

1314
using namespace ::std::literals::string_view_literals;
@@ -55,6 +56,129 @@ srcml_input_src::srcml_input_src(std::string_view other) {
5556
resource = resource.substr(0, query_pos);
5657
}
5758
}
59+
60+
// local files, and urls, may carry a trailing range suffix, e.g. "main.cpp:191", "main.cpp:191:5", or "main.cpp:191:5-222:12"
61+
if (protocol == "file"sv || curl_supported(protocol)) {
62+
63+
// a number at position i of s, advancing i, where a position of zero is invalid,
64+
// and a number too large for a position is not part of a suffix at all
65+
auto number = [](std::string_view s, size_t& i) -> std::optional<int> {
66+
67+
size_t digits = i;
68+
while (digits < s.size() && s[digits] >= '0' && s[digits] <= '9')
69+
++digits;
70+
71+
if (digits == i)
72+
return std::nullopt;
73+
74+
int position = 0;
75+
try {
76+
position = std::stoi(std::string(s.substr(i, digits - i)));
77+
} catch (const std::out_of_range&) {
78+
return std::nullopt;
79+
}
80+
81+
i = digits;
82+
83+
return position != 0 ? position : INVALID_POSITION;
84+
};
85+
86+
// "LINE[:COLUMN][-[LINE[:COLUMN]]]" or "-LINE[:COLUMN]", the part of a suffix after the colon
87+
auto parse_suffix = [&number](std::string_view s, int& line, int& column, int& end_line, int& end_column) {
88+
89+
line = 1;
90+
column = 0;
91+
end_line = 0;
92+
end_column = 0;
93+
94+
size_t i = 0;
95+
bool isstart = !s.empty() && s[0] != '-';
96+
97+
if (isstart) {
98+
99+
auto n = number(s, i);
100+
if (!n)
101+
return false;
102+
103+
line = *n;
104+
105+
if (i < s.size() && s[i] == ':') {
106+
107+
auto c = number(s, ++i);
108+
if (!c)
109+
return false;
110+
111+
column = *c;
112+
}
113+
}
114+
115+
// without a range, the end is the end of the start line
116+
if (i == s.size()) {
117+
118+
end_line = line;
119+
120+
return isstart;
121+
}
122+
123+
if (s[i] != '-')
124+
return false;
125+
126+
// an open range ends at the end of the file
127+
if (++i == s.size()) {
128+
129+
end_line = END_OF_FILE;
130+
131+
return isstart;
132+
}
133+
134+
auto n = number(s, i);
135+
if (!n)
136+
return false;
137+
138+
end_line = *n;
139+
140+
if (i < s.size() && s[i] == ':') {
141+
142+
auto c = number(s, ++i);
143+
if (!c)
144+
return false;
145+
146+
end_column = *c;
147+
}
148+
149+
return i == s.size();
150+
};
151+
152+
// the suffix starts at the first colon of the filename whose remainder is an entire suffix
153+
size_t base = resource.find_last_of("/\\");
154+
for (size_t colon_pos = resource.find(':', base == std::string::npos ? 0 : base + 1);
155+
colon_pos != std::string::npos; colon_pos = resource.find(':', colon_pos + 1)) {
156+
157+
int suffix_line = 0;
158+
int suffix_column = 0;
159+
int suffix_end_line = 0;
160+
int suffix_end_column = 0;
161+
162+
if (!parse_suffix(std::string_view(resource).substr(colon_pos + 1), suffix_line, suffix_column, suffix_end_line, suffix_end_column))
163+
continue;
164+
165+
// a file of that exact name, suffix and all, is a filename, not a suffix
166+
struct stat s;
167+
if (protocol != "file"sv || stat(resource.data(), &s) != 0) {
168+
169+
line = suffix_line;
170+
column = suffix_column;
171+
end_line = suffix_end_line;
172+
end_column = suffix_end_column;
173+
174+
resource = resource.substr(0, colon_pos);
175+
filename = src_prefix_add_uri(protocol, resource);
176+
}
177+
178+
break;
179+
}
180+
}
181+
58182
exists = false;
59183

60184
if (protocol == "file"sv) {

‎src/client/srcml_input_src.hpp‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <archive.h>
2424
#include <sys/stat.h>
2525
#include <numeric>
26+
#include <climits>
2627
#include <libarchive_utilities.hpp>
2728
#include <memory>
2829

@@ -81,6 +82,13 @@ class srcml_input_src {
8182
bool isdirectoryform = false;
8283
bool skip = false;
8384
int unit = 0;
85+
// optional source range from a "filename:LINE:COLUMN-LINE:COLUMN" suffix, where a position of 0 is not given
86+
static const int INVALID_POSITION = -1;
87+
static const int END_OF_FILE = INT_MAX;
88+
int line = 0;
89+
int column = 0;
90+
int end_line = 0;
91+
int end_column = 0;
8492

8593
// pre-read of stdin
8694
bool preReadLibarchive = false;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: GPL-3.0-only
3+
#
4+
# @file remote_suffix_file.sh
5+
#
6+
# @copyright Copyright (C) 2026 srcML, LLC. (www.srcML.org)
7+
8+
# test framework
9+
source $(dirname "$0")/framework_test.sh
10+
11+
# test a position suffix on a remote source file
12+
13+
defineXML output <<- 'STDOUT'
14+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
15+
<unit xmlns="http://www.srcML.org/srcML/src" revision="REVISION" language="C++" filename="raw.githubusercontent.com/srcML/test-data/1.0.0/source/a.cpp"><expr_stmt><expr><name>a</name></expr>;</expr_stmt>
16+
</unit>
17+
STDOUT
18+
19+
defineXML empty <<- 'STDOUT'
20+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
21+
<unit xmlns="http://www.srcML.org/srcML/src" revision="REVISION" language="C++" filename="raw.githubusercontent.com/srcML/test-data/1.0.0/source/a.cpp"/>
22+
STDOUT
23+
24+
# the only line of the remote file
25+
srcml https://raw.githubusercontent.com/srcML/test-data/1.0.0/source/a.cpp:1
26+
check "$output"
27+
28+
srcml https://raw.githubusercontent.com/srcML/test-data/1.0.0/source/a.cpp:1-1 --output-src
29+
check "a;\n"
30+
31+
srcml https://raw.githubusercontent.com/srcML/test-data/1.0.0/source/a.cpp:1- --output-src
32+
check "a;\n"
33+
34+
# a line past the end of the remote file has no source
35+
srcml https://raw.githubusercontent.com/srcML/test-data/1.0.0/source/a.cpp:9
36+
check "$empty"
37+
38+
# lines and columns start at one
39+
srcml https://raw.githubusercontent.com/srcML/test-data/1.0.0/source/a.cpp:0
40+
check_exit 1

0 commit comments

Comments
 (0)