Intermediate

I/O Operations in ABAP

How ABAP handles input and output - the WRITE list buffer, string-template formatting, parsing input records with SPLIT and CONCATENATE, tabular output from internal tables, selection-screen input, and application-server file I/O

Input and output in ABAP look very different from the print/read/fopen trio you find in most languages, because ABAP was never a console or shell language - it is a server-side, business-application language that runs inside an SAP application server. There is no terminal to read a line from and no working directory to open a file in. Instead, ABAP’s I/O grew out of two needs: producing formatted business reports and moving data between programs, screens, and the database.

That heritage shapes everything in this tutorial. Output goes to the list buffer through WRITE (introduced in Hello World) and, in modern code, through string templates for precise formatting. “Input” in the classic sense comes from a selection screen - the PARAMETERS and SELECT-OPTIONS fields a user fills in before the report runs - not from standard input. Genuine file I/O happens against the application server’s file system with OPEN DATASET, while database access (the most common ABAP “I/O” of all) uses Open SQL.

In this tutorial you’ll build one runnable program that exercises the parts open-abap supports on Node.js: detailed console output, string-template formatting with fixed decimals, parsing a delimited input record with SPLIT, reassembling it with CONCATENATE, and producing a formatted table from an internal table. After the runnable example, two Real-World ABAP sections show how selection-screen input and application-server file I/O work in an actual SAP system - syntax you’ll read constantly in real code but that needs a live SAP server to execute.

Output: WRITE and string templates

ABAP gives you two layers of output. WRITE sends a field to the list buffer and is ideal for fixed text and simple lines; a leading / starts a new line. For anything that mixes literals with variables - the common case - string templates (|...| with { } placeholders) are clearer and give you formatting options such as DECIMALS, WIDTH, and ALIGN. Because templates render numbers with a fixed . decimal separator regardless of user settings, their output is predictable, which is exactly what you want when formatting money or quantities.

“Input”: parsing records

Since open-abap has no selection screen, the runnable example demonstrates the other half of input handling that is portable: taking a raw record and breaking it into fields. SPLIT ... AT ... INTO is ABAP’s tokenizer, and CONCATENATE ... SEPARATED BY reassembles fields into a single string. This record-in, fields-out, line-out pattern is the heart of most ABAP I/O, whether the record originally came from a screen, a file, or a database row.

The runnable example

The program below produces a small order confirmation: a header, a formatted line of values, a parsed employee record, and a totaled item table. Every value is computed so the output is fully deterministic.

Create a file named io_operations.abap:

 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
REPORT zio.

" ============================================================
" 1. Console output - WRITE and the list buffer
" ============================================================
WRITE 'Order Confirmation'.
WRITE / '=================='.

" ============================================================
" 2. Formatted output with string templates
" ============================================================
DATA(lv_customer) = `ACME Corp`.        " backticks => string type

DATA lv_unit_price TYPE p LENGTH 8 DECIMALS 2.
DATA lv_quantity   TYPE i.
lv_unit_price = '29.95'.
lv_quantity   = 3.

DATA lv_amount TYPE p LENGTH 8 DECIMALS 2.
lv_amount = lv_unit_price * lv_quantity.

" { } embeds an expression; DECIMALS pins the fraction digits
WRITE / |Customer  : { lv_customer }|.
WRITE / |Unit price: { lv_unit_price DECIMALS = 2 } EUR|.
WRITE / |Quantity  : { lv_quantity }|.
WRITE / |Amount    : { lv_amount DECIMALS = 2 } EUR|.

" ============================================================
" 3. Reading and parsing an input record
" ============================================================
" In real SAP this record would arrive from a selection screen
" or a file; here we parse a delimited string - a core I/O task.
DATA(lv_record) = `Alice,Engineering,7`.
SPLIT lv_record AT ',' INTO DATA(lv_name)
                            DATA(lv_dept)
                            DATA(lv_years).

WRITE / '------------------'.
WRITE / |Name : { lv_name }|.
WRITE / |Dept : { lv_dept }|.
WRITE / |Years: { lv_years }|.

" Reassemble fields into one line
DATA lv_summary TYPE string.
CONCATENATE lv_name lv_dept lv_years
  INTO lv_summary SEPARATED BY ` | `.
WRITE / |Summary: { lv_summary }|.

" ============================================================
" 4. Tabular output from an internal table
" ============================================================
TYPES: BEGIN OF ty_item,
         name TYPE string,
         qty  TYPE i,
         cost TYPE p LENGTH 8 DECIMALS 2,
       END OF ty_item.
DATA lt_items TYPE TABLE OF ty_item.

APPEND VALUE #( name = 'Keyboard' qty = 3 cost = '29.95' )  TO lt_items.
APPEND VALUE #( name = 'Monitor'  qty = 1 cost = '149.00' ) TO lt_items.
APPEND VALUE #( name = 'Cable'    qty = 5 cost = '4.50' )   TO lt_items.

DATA lv_grand TYPE p LENGTH 10 DECIMALS 2.

WRITE / '------------------'.
LOOP AT lt_items INTO DATA(ls_item).
  DATA lv_line TYPE p LENGTH 10 DECIMALS 2.
  lv_line  = ls_item-qty * ls_item-cost.
  lv_grand = lv_grand + lv_line.
  WRITE / |{ ls_item-name }: { ls_item-qty } x { ls_item-cost DECIMALS = 2 } = { lv_line DECIMALS = 2 }|.
ENDLOOP.

WRITE / |Grand total: { lv_grand DECIMALS = 2 } EUR|.

Running with Docker

Run the program with the same open-abap transpiler pattern from the Hello World tutorial - this time pointing at io_operations.abap (program name zio).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Pull the Node.js image
docker pull node:20

# Run the ABAP program
docker run --rm -v $(pwd):/work node:20 sh -c '
cd /work && mkdir -p abap_project/src && cd abap_project && \
npm init -y >/dev/null 2>&1 && \
npm install --silent @abaplint/transpiler-cli @abaplint/runtime 2>/dev/null && \
echo "{\"input_folder\":\"src\",\"output_folder\":\"output\",\"write_unit_tests\":false,\"options\":{\"ignoreSyntaxCheck\":true}}" > abap_transpile.json && \
echo "{\"global\":{\"files\":\"/src/**/*.*\"},\"syntax\":{\"version\":\"v702\",\"errorNamespace\":\".\"}}" > abaplint.json && \
cp /work/io_operations.abap src/zio.prog.abap && \
echo "<?xml version=\"1.0\"?><abapGit version=\"v1.0.0\"><asx:abap xmlns:asx=\"http://www.sap.com/abapxml\"><asx:values><PROGDIR><NAME>ZIO</NAME><SUBC>1</SUBC></PROGDIR></asx:values></asx:abap></abapGit>" > src/zio.prog.xml && \
./node_modules/.bin/abap_transpile >/dev/null && \
echo "import runtime from \"@abaplint/runtime\";globalThis.abap = new runtime.ABAP();await import(\"./output/zio.prog.mjs\");" > run.mjs && \
node run.mjs && cd /work && rm -rf abap_project'

Expected Output

Order Confirmation
==================
Customer  : ACME Corp
Unit price: 29.95 EUR
Quantity  : 3
Amount    : 89.85 EUR
------------------
Name : Alice
Dept : Engineering
Years: 7
Summary: Alice | Engineering | 7
------------------
Keyboard: 3 x 29.95 = 89.85
Monitor: 1 x 149.00 = 149.00
Cable: 5 x 4.50 = 22.50
Grand total: 261.35 EUR

Notes on Behavior

  • The first WRITE has no leading /, so “Order Confirmation” lands on the first list line; every later WRITE / starts a new line - the list-buffer behavior from Hello World.
  • Backticks (`) create a string; single quotes (') create a fixed-length c literal. Using backticks for lv_customer and lv_record keeps them as true strings, which SPLIT and CONCATENATE work with cleanly.
  • { lv_amount DECIMALS = 2 } formats a packed number with exactly two fraction digits and a . separator. 29.95 * 3 is 89.85, and 5 * 4.50 is 22.50 - packed arithmetic stays exact, which is why ABAP uses type p for money.
  • SPLIT lv_record AT ',' INTO DATA(...) DATA(...) DATA(...) declares its three targets inline and fills them with the comma-separated fields.
  • CONCATENATE ... SEPARATED BY \ | `glues the fields back together with a" | “delimiter, producingAlice | Engineering | 7`.
  • The LOOP AT ... INTO reads each table row into a work area; the running lv_grand accumulates each line total to 261.35.

Real-World ABAP: Selection-Screen Input

On an actual SAP system, a report does not read stdin - it presents a selection screen. You declare input fields with PARAMETERS (single values) and SELECT-OPTIONS (ranges), and the runtime renders an input mask before START-OF-SELECTION runs. The user’s entries are simply available as variables:

1
2
3
4
5
6
7
8
REPORT zinput.

PARAMETERS: p_cust TYPE string LOWER CASE,
            p_qty  TYPE i DEFAULT 1.

START-OF-SELECTION.
  WRITE / |Customer: { p_cust }|.
  WRITE / |Quantity: { p_qty }|.

The obsolete ACCEPT/PARAMETER console statements exist only for legacy character-mode use; modern code always uses selection screens, classic dialog (PAI/PBO) screens, or Fiori/RAP services for input. The transpiler used here has no screen layer, so this snippet illustrates real-SAP syntax rather than something runnable on Node.js.

Real-World ABAP: File I/O on the Application Server

ABAP reads and writes files on the application server with the dataset statements - the closest analog to open/read/write/close. After each statement you check sy-subrc for success, the ABAP convention for I/O error handling:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
DATA lv_text TYPE string.

" Write a file on the application server
OPEN DATASET '/tmp/orders.txt' FOR OUTPUT IN TEXT MODE ENCODING UTF-8.
IF sy-subrc = 0.
  TRANSFER 'Keyboard;3;29.95' TO '/tmp/orders.txt'.
  CLOSE DATASET '/tmp/orders.txt'.
ENDIF.

" Read it back, line by line
OPEN DATASET '/tmp/orders.txt' FOR INPUT IN TEXT MODE ENCODING UTF-8.
IF sy-subrc = 0.
  DO.
    READ DATASET '/tmp/orders.txt' INTO lv_text.
    IF sy-subrc <> 0.
      EXIT.                       " end of file
    ENDIF.
    WRITE / lv_text.
  ENDDO.
  CLOSE DATASET '/tmp/orders.txt'.
ENDIF.

Note that OPEN DATASET targets the SAP server’s file system, not the user’s PC - a deliberate security boundary. For client-side files you use function modules such as GUI_UPLOAD / GUI_DOWNLOAD, and for the most common I/O of all - persistent business data - you use Open SQL (SELECT / INSERT / UPDATE) against database tables. These statements need a live SAP application server, so they are shown here for reference rather than run under open-abap.

Key Concepts

  • WRITE targets the list buffer, not a console; a leading / starts a new line, a heritage of ABAP’s report-generation origins.
  • String templates (|...|) are the modern way to format output, embedding expressions with { } and offering options like DECIMALS, WIDTH, and ALIGN.
  • Type p (packed) with DECIMALS gives exact decimal arithmetic for money; DECIMALS = 2 in a template pins the displayed fraction digits.
  • SPLIT ... AT ... INTO tokenizes a record into fields; CONCATENATE ... SEPARATED BY reassembles fields into a line - the core of record-oriented I/O.
  • Input comes from selection screens (PARAMETERS, SELECT-OPTIONS), classic dialog screens, or Fiori/RAP services - ABAP is a server language with no stdin.
  • File I/O uses OPEN DATASET / TRANSFER / READ DATASET / CLOSE DATASET against the application server, with sy-subrc checked after each operation for error handling.
  • Database access is the most common ABAP I/O, handled by Open SQL rather than file statements.
  • open-abap runs console output, templates, and string parsing on Node.js, while screens, datasets, and Open SQL need a real SAP system.

Running Today

All examples can be run using Docker:

docker pull node:20
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining