mirror of
https://github.com/sasjs/core.git
synced 2025-12-12 15:04:36 +00:00
Compare commits
322 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42a46b32e9 | ||
|
|
3b395b3ae5 | ||
|
|
9e84e47563 | ||
|
|
aff29427e2 | ||
|
|
474f1b3cc6 | ||
|
|
22bf8065bb | ||
|
|
7bb089e269 | ||
|
|
a6e9158814 | ||
|
|
19a1336720 | ||
|
|
79d5d42e6b | ||
|
|
2d81de5841 | ||
|
|
107ab836d6 | ||
|
|
5225e74465 | ||
|
|
39253d2828 | ||
|
|
171c169537 | ||
|
|
76af9fa33c | ||
|
|
37ccc8a2aa | ||
|
|
d0a0274990 | ||
|
|
b3d374f1b1 | ||
|
|
1c4458faf6 | ||
|
|
96e1d146f4 | ||
|
|
aadc4fb83d | ||
|
|
988ee89cdb | ||
|
|
51cbfbf4bc | ||
|
|
4b69e91362 | ||
|
|
8f9715035a | ||
|
|
35ddccaa16 | ||
|
|
cb0ddfb61c | ||
|
|
c3b6f06b3a | ||
|
|
8046d5a0b1 | ||
|
|
aed07f2943 | ||
|
|
5bf87a78b8 | ||
|
|
0851523d18 | ||
|
|
9e2de81dae | ||
|
|
4887f355c8 | ||
|
|
9b32e6e3f2 | ||
|
|
74790ec80e | ||
|
|
afd8a754b4 | ||
|
|
bc1f7b3baa | ||
|
|
51690e68dc | ||
|
|
0fa076cb73 | ||
|
|
6506993704 | ||
|
|
a69db2ebfb | ||
|
|
d72ca7cb24 | ||
|
|
52dfa7b8f7 | ||
|
|
dae03c5730 | ||
|
|
14efe5d3fd | ||
|
|
653244d737 | ||
|
|
086831b3f5 | ||
|
|
6eca585fc1 | ||
|
|
f6ba36fc28 | ||
|
|
7406288d79 | ||
|
|
2e7fcbe5b8 | ||
|
|
3e7b9f8c14 | ||
|
|
e9189ccc06 | ||
|
|
8c00d715c2 | ||
|
|
d47a369cdf | ||
|
|
52bf6019fd | ||
|
|
25e61fd8ef | ||
|
|
3038be83a0 | ||
|
|
6e2447c70a | ||
|
|
486aba84ca | ||
|
|
b5944181e1 | ||
|
|
3f69cf506a | ||
|
|
6013897c50 | ||
|
|
27cf2a2532 | ||
|
|
d096cbddeb | ||
|
|
117503f214 | ||
|
|
5cc5fae750 | ||
|
|
d93032e1a9 | ||
|
|
507557b2cb | ||
|
|
ee5c3c185a | ||
|
|
e5592a2eb2 | ||
|
|
59200a6e73 | ||
|
|
f468f60ae1 | ||
|
|
9f60d827b6 | ||
|
|
5c936ddb65 | ||
|
|
d0bde62594 | ||
|
|
ada9192337 | ||
|
|
6161f588a9 | ||
|
|
67079d8c17 | ||
|
|
75bd39adb0 | ||
|
|
078bdbeecf | ||
|
|
8ddb86785c | ||
|
|
005af0ecf8 | ||
|
|
bc410a9135 | ||
|
|
fc8ba2e36c | ||
|
|
756441384a | ||
|
|
10f9eecf9e | ||
| 470ebb50a7 | |||
|
|
26cd5d9d31 | ||
|
|
0b694bb878 | ||
|
|
b403c02bba | ||
|
|
0b555bb31c | ||
|
|
40b513a9e3 | ||
|
|
4eacf4deae | ||
|
|
5824423c13 | ||
|
|
ce5bfd41dc | ||
|
|
0c67a07e42 | ||
|
|
187504600a | ||
|
|
658d67feaa | ||
|
|
5207a77591 | ||
|
|
4456adf1dc | ||
|
|
03962c2a50 | ||
|
|
6d2fc7e265 | ||
|
|
39b2e7c5f9 | ||
|
|
f99adf5c3e | ||
|
|
69f8e91a2d | ||
|
|
5b5d01993f | ||
|
|
00fa464a7c | ||
|
|
a5baf46233 | ||
|
|
d63d2a4ec1 | ||
|
|
900f694065 | ||
|
|
838324c15e | ||
|
|
e3205ec06c | ||
|
|
154a33434e | ||
|
|
bfa1bbaeb1 | ||
|
|
1f0128aec4 | ||
|
|
69f03f4e14 | ||
|
|
a932f321d8 | ||
|
|
21200c11c1 | ||
|
|
825c97c49c | ||
|
|
f301899269 | ||
|
|
fc81f62d2f | ||
|
|
93aea5ed02 | ||
|
|
55d4c7238a | ||
|
|
cd75bf263a | ||
|
|
929a1a9974 | ||
|
|
7cafb4fb36 | ||
|
|
a8d222a0f8 | ||
|
|
ac0ddf38b0 | ||
|
|
ecd389c935 | ||
|
|
06a5ea06f8 | ||
|
|
955471ed3c | ||
|
|
c8d3b43b12 | ||
|
|
3e313b06a9 | ||
|
|
d7371a4505 | ||
|
|
32a6d15c2e | ||
|
|
b109e7cead | ||
| d291d3e287 | |||
|
|
5a2968e798 | ||
|
|
120ad9a7da | ||
|
|
67a81b2690 | ||
|
|
506cf1812f | ||
|
|
8cc0eb0dd7 | ||
|
|
4c1f69da3a | ||
|
|
f160ebe705 | ||
|
|
3f49925d01 | ||
|
|
53ed5dc916 | ||
|
|
808b24e31b | ||
|
|
c51c9c2ca9 | ||
|
|
4d6edf5566 | ||
|
|
41a24677f5 | ||
|
|
e7d8d8ffb3 | ||
|
|
60d23dd618 | ||
|
|
d2764c3cd1 | ||
|
|
f9f4355143 | ||
|
|
18bc6c889d | ||
|
|
42a16ef496 | ||
|
|
8178b801fb | ||
|
|
ce331a23c8 | ||
|
|
1cc9213467 | ||
|
|
aabbf4d0f9 | ||
|
|
2bea8be70d | ||
|
|
9b2368443e | ||
|
|
7915ba2c41 | ||
|
|
cc61e48868 | ||
|
|
62db83dcf6 | ||
|
|
7ea9e0f8e9 | ||
|
|
1c852515f5 | ||
|
|
b7e677bd8e | ||
|
|
f47f0d2cee | ||
|
|
3d0f426a98 | ||
|
|
2cb51f6164 | ||
|
|
b73bf998da | ||
|
|
c9ad38ee98 | ||
|
|
76b1b951c0 | ||
|
|
996054b17a | ||
|
|
7fca3d4e3f | ||
|
|
c4e599c861 | ||
|
|
0f6ff2cc1e | ||
|
|
7cac4c71fb | ||
|
|
1322bdab92 | ||
|
|
f201df606a | ||
|
|
a56fce86b1 | ||
|
|
41ccc5fdd9 | ||
|
|
b2877bd493 | ||
|
|
df8f8893e7 | ||
|
|
27fbdf193b | ||
|
|
6ae892989d | ||
|
|
39a7b332da | ||
|
|
c81794b542 | ||
|
|
e456da846a | ||
|
|
5c144be05b | ||
|
|
055669c133 | ||
|
|
4b67e13b24 | ||
|
|
f1ec3eda81 | ||
|
|
f2d5859675 | ||
|
|
ea057d4655 | ||
|
|
26c085b354 | ||
|
|
d13ac52739 | ||
|
|
bbbc28ad6d | ||
|
|
530cd6e95c | ||
|
|
c4e17e43e8 | ||
|
|
fed217eec3 | ||
|
|
1934dc8332 | ||
|
|
9de512cfc7 | ||
|
|
cadafcc86b | ||
|
|
5f805b006f | ||
|
|
c6b65366b7 | ||
|
|
51ddd9c1e5 | ||
|
|
20bf3b86af | ||
|
|
de67cd329b | ||
|
|
779e4942c7 | ||
|
|
a69a1ac7f0 | ||
|
|
2a644d6c2b | ||
|
|
843930c666 | ||
|
|
90d69af7ee | ||
|
|
b7bafb49f4 | ||
|
|
2fa9e48286 | ||
|
|
5cee93c7bd | ||
|
|
1a595c64c6 | ||
|
|
2c901831b7 | ||
|
|
28209950ab | ||
|
|
44069e9867 | ||
|
|
e26af5c09a | ||
|
|
4ee13c9389 | ||
|
|
15f903aa42 | ||
|
|
58a0cce39e | ||
|
|
9a5574ea0e | ||
|
|
e6146dcbcf | ||
|
|
583c7e0c83 | ||
|
|
223bdd5983 | ||
|
|
aef14543f0 | ||
|
|
c88764c1d8 | ||
|
|
2c952c8b01 | ||
|
|
de3610d1aa | ||
|
|
d35d597437 | ||
|
|
3e8deda008 | ||
|
|
a27496c7b3 | ||
|
|
265389befc | ||
|
|
db2531e0b3 | ||
|
|
5e77494aa6 | ||
|
|
6a2ac51925 | ||
|
|
f625b04189 | ||
|
|
68aee776d3 | ||
|
|
38d2195d32 | ||
|
|
4e564b5409 | ||
|
|
298acc4e50 | ||
|
|
af98909753 | ||
|
|
b9d33b38bf | ||
|
|
b61b5f1856 | ||
|
|
805474bb46 | ||
|
|
61701f3c6a | ||
|
|
f20d7476bf | ||
|
|
04a3189a89 | ||
|
|
b1380983ec | ||
|
|
b4834f9b40 | ||
|
|
1b5ad93cad | ||
|
|
f2942f2032 | ||
|
|
4198448b81 | ||
|
|
47a33452e0 | ||
|
|
fb21a0adfd | ||
|
|
e01b06b640 | ||
|
|
24380ddf26 | ||
|
|
1ef42d45af | ||
|
|
6ee13a2779 | ||
|
|
ffd2e135dc | ||
|
|
7f2ad5fc66 | ||
| ff1eb54cc3 | |||
|
|
d6235c6357 | ||
|
|
98118adb9a | ||
|
|
369c4412f3 | ||
|
|
7d7608f06c | ||
|
|
3791cb8a2c | ||
|
|
ff82f7d75c | ||
|
|
fdd566e8ce | ||
|
|
328f8c260b | ||
|
|
029169ac80 | ||
| 66ff1de7a9 | |||
| 053290c7df | |||
| af71a5e53b | |||
| ecdce86287 | |||
| ba1272aaf7 | |||
| d6056b9397 | |||
|
|
00511c72c2 | ||
|
|
1d6f04fd56 | ||
| af4dbb5632 | |||
|
|
f48c291dce | ||
|
|
18be74a1c2 | ||
|
|
456d10a90e | ||
|
|
a7fdb52231 | ||
|
|
066ed00e44 | ||
|
|
49fbc210ad | ||
|
|
951aa474f2 | ||
|
|
961dd54ee0 | ||
|
|
921354dac7 | ||
|
|
48212f8797 | ||
| cb8992dade | |||
| 7dec3120be | |||
| 9568b17f20 | |||
| 0a38056c69 | |||
|
|
096bf4fa11 | ||
|
|
030c4a4fc1 | ||
| 1b70205cab | |||
| 539447ed06 | |||
| e3c333ea39 | |||
| ae72446f85 | |||
| 2b6bf4bd02 | |||
| 6dbb3760e0 | |||
| 200d9a5761 | |||
|
|
01a9a5b823 | ||
|
|
35eadd0e9d | ||
| 5cdca95216 | |||
|
|
81b75a32ed | ||
| b7f5a2ec00 | |||
| db859bbf1d | |||
| 27b56e8efd | |||
| 28ea218d02 | |||
| f356e1f351 | |||
| 4b375e0b8c | |||
| 7db207dd1c |
124
.all-contributorsrc
Normal file
124
.all-contributorsrc
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"projectName": "core",
|
||||||
|
"projectOwner": "sasjs",
|
||||||
|
"repoType": "github",
|
||||||
|
"repoHost": "https://github.com",
|
||||||
|
"files": [
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"imageSize": 100,
|
||||||
|
"commit": false,
|
||||||
|
"commitConvention": "angular",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"login": "allanbowe",
|
||||||
|
"name": "Allan Bowe",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
|
||||||
|
"profile": "https://github.com/allanbowe",
|
||||||
|
"contributions": [
|
||||||
|
"business",
|
||||||
|
"code",
|
||||||
|
"content",
|
||||||
|
"doc",
|
||||||
|
"infra",
|
||||||
|
"maintenance",
|
||||||
|
"mentoring",
|
||||||
|
"question",
|
||||||
|
"review",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "rafgag",
|
||||||
|
"name": "rafgag",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/69139928?v=4",
|
||||||
|
"profile": "https://github.com/rafgag",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "tmoody",
|
||||||
|
"name": "Trevor Moody",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/79837106?v=4",
|
||||||
|
"profile": "https://github.com/tmoody",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "krishna-acondy",
|
||||||
|
"name": "Krishna Acondy",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/2980428?v=4",
|
||||||
|
"profile": "https://krishna-acondy.io/",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"infra",
|
||||||
|
"blog",
|
||||||
|
"content",
|
||||||
|
"ideas",
|
||||||
|
"video"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "saadjutt01",
|
||||||
|
"name": "Muhammad Saad ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
|
||||||
|
"profile": "https://github.com/saadjutt01",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"ideas"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "YuryShkoda",
|
||||||
|
"name": "Yury Shkoda",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
|
||||||
|
"profile": "https://www.erudicat.com/",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"infra",
|
||||||
|
"video"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "medjedovicm",
|
||||||
|
"name": "Mihajlo Medjedovic",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
|
||||||
|
"profile": "https://github.com/medjedovicm",
|
||||||
|
"contributions": [
|
||||||
|
"infra"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "kkchandok",
|
||||||
|
"name": "kkchandok",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/46090627?v=4",
|
||||||
|
"profile": "https://github.com/kkchandok",
|
||||||
|
"contributions": [
|
||||||
|
"ideas"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "VladislavParhomchik",
|
||||||
|
"name": "Vladislav Parhomchik",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
|
||||||
|
"profile": "https://github.com/VladislavParhomchik",
|
||||||
|
"contributions": [
|
||||||
|
"test",
|
||||||
|
"review"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "vznesh",
|
||||||
|
"name": "Vignesh T.",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/28916792?v=4",
|
||||||
|
"profile": "https://github.com/vznesh",
|
||||||
|
"contributions": [
|
||||||
|
"bug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contributorsPerLine": 7,
|
||||||
|
"skipCi": true
|
||||||
|
}
|
||||||
18
.git-hooks/commit-msg
Executable file
18
.git-hooks/commit-msg
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
RED="\033[1;31m"
|
||||||
|
GREEN="\033[1;32m"
|
||||||
|
|
||||||
|
# Get the commit message (the parameter we're given is just the path to the
|
||||||
|
# temporary file which holds the message).
|
||||||
|
commit_message=$(cat "$1")
|
||||||
|
|
||||||
|
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$") then
|
||||||
|
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${RED}❌ Commit message does not meet the Conventional Commit standard!"
|
||||||
|
echo "An example of a valid message is:"
|
||||||
|
echo " feat(login): add the 'remember me' button"
|
||||||
|
echo "ℹ More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
|
||||||
|
exit 1
|
||||||
2
.git-hooks/pre-commit
Executable file
2
.git-hooks/pre-commit
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
sasjs lint
|
||||||
30
.github/vpn/config.ovpn
vendored
Normal file
30
.github/vpn/config.ovpn
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
cipher AES-256-CBC
|
||||||
|
setenv FORWARD_COMPATIBLE 1
|
||||||
|
client
|
||||||
|
server-poll-timeout 4
|
||||||
|
nobind
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 443 tcp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
remote vpn.analytium.co.uk 1194 udp
|
||||||
|
dev tun
|
||||||
|
dev-type tun
|
||||||
|
ns-cert-type server
|
||||||
|
setenv opt tls-version-min 1.0 or-highest
|
||||||
|
reneg-sec 604800
|
||||||
|
sndbuf 0
|
||||||
|
rcvbuf 0
|
||||||
|
# NOTE: LZO commands are pushed by the Access Server at connect time.
|
||||||
|
# NOTE: The below line doesn't disable LZO.
|
||||||
|
comp-lzo no
|
||||||
|
verb 3
|
||||||
|
setenv PUSH_PEER_INFO
|
||||||
|
|
||||||
|
ca ca.crt
|
||||||
|
cert user.crt
|
||||||
|
key user.key
|
||||||
|
tls-auth tls.key 1
|
||||||
84
.github/workflows/run-tests.yml
vendored
Normal file
84
.github/workflows/run-tests.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Node.js CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [lts/fermium]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Write VPN Files
|
||||||
|
run: |
|
||||||
|
echo "$CA_CRT" > .github/vpn/ca.crt
|
||||||
|
echo "$USER_CRT" > .github/vpn/user.crt
|
||||||
|
echo "$USER_KEY" > .github/vpn/user.key
|
||||||
|
echo "$TLS_KEY" > .github/vpn/tls.key
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CA_CRT: ${{ secrets.CA_CRT}}
|
||||||
|
USER_CRT: ${{ secrets.USER_CRT }}
|
||||||
|
USER_KEY: ${{ secrets.USER_KEY }}
|
||||||
|
TLS_KEY: ${{ secrets.TLS_KEY }}
|
||||||
|
|
||||||
|
- name: Install Open VPN
|
||||||
|
run: |
|
||||||
|
sudo apt install apt-transport-https
|
||||||
|
sudo wget https://swupdate.openvpn.net/repos/openvpn-repo-pkg-key.pub
|
||||||
|
sudo apt-key add openvpn-repo-pkg-key.pub
|
||||||
|
sudo wget -O /etc/apt/sources.list.d/openvpn3.list https://swupdate.openvpn.net/community/openvpn3/repos/openvpn3-focal.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install openvpn3
|
||||||
|
|
||||||
|
- name: Start Open VPN 3
|
||||||
|
run: openvpn3 session-start --config .github/vpn/config.ovpn
|
||||||
|
|
||||||
|
- name: Install Doxygen
|
||||||
|
run: sudo apt-get install doxygen
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Check code style
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Add client
|
||||||
|
run: echo "CLIENT=${{secrets.CLIENT}}"> .env.viya
|
||||||
|
|
||||||
|
- name: Add secret
|
||||||
|
run: echo "SECRET=${{secrets.SECRET}}" >> .env.viya
|
||||||
|
|
||||||
|
- name: Add access token
|
||||||
|
run: echo "ACCESS_TOKEN=${{secrets.ACCESS_TOKEN}}" >> .env.viya
|
||||||
|
|
||||||
|
- name: Add refresh token
|
||||||
|
run: echo "REFRESH_TOKEN=${{secrets.REFRESH_TOKEN}}" >> .env.viya
|
||||||
|
|
||||||
|
- name: Build Project
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Run SASjs tests
|
||||||
|
run: npm run test
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
CLIENT: ${{secrets.CLIENT}}
|
||||||
|
SECRET: ${{secrets.SECRET}}
|
||||||
|
SAS_USERNAME: ${{secrets.SAS_USERNAME}}
|
||||||
|
SAS_PASSWORD: ${{secrets.SAS_PASSWORD}}
|
||||||
|
SERVER_URL: ${{secrets.SERVER_URL}}
|
||||||
|
SERVER_TYPE: ${{secrets.SERVER_TYPE}}
|
||||||
|
ACCESS_TOKEN: ${{secrets.ACCESS_TOKEN}}
|
||||||
|
REFRESH_TOKEN: ${{secrets.REFRESH_TOKEN}}
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,3 +1,13 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
sasjsresults/
|
||||||
|
|
||||||
|
# avoid filenames with spaces being committed to source control
|
||||||
|
**\ **
|
||||||
|
|
||||||
|
# ignore the mc_* files - containing macros for individual libraries
|
||||||
|
mc_*
|
||||||
|
|
||||||
|
# ignore .env files as they can contain sasjs access tokens
|
||||||
|
*.env*
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
|
|
||||||
FROM gitpod/workspace-full
|
FROM gitpod/workspace-full
|
||||||
|
|
||||||
RUN sudo apt-get update \
|
RUN sudo apt-get update \
|
||||||
&& sudo apt-get install -y \
|
&& sudo apt-get install -y \
|
||||||
doxygen \
|
doxygen \
|
||||||
&& npm i -g npm@latest \
|
|
||||||
&& npm i -g @sasjs/cli \
|
|
||||||
&& npm i \
|
|
||||||
&& sudo rm -rf /var/lib/apt/lists/*
|
&& sudo rm -rf /var/lib/apt/lists/*
|
||||||
21
.gitpod.yml
21
.gitpod.yml
@@ -1,8 +1,25 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- init: npm i && clear
|
- init: nvm install --lts && npm i -g @sasjs/cli
|
||||||
|
|
||||||
image:
|
image:
|
||||||
file: .gitpod.dockerfile
|
file: .gitpod.dockerfile
|
||||||
vscode:
|
vscode:
|
||||||
extensions:
|
extensions:
|
||||||
- sasjs.sasjs-for-vscode@1.4.3:LY5VLf6H5R3u7nqVRnCQRw==
|
- sasjs.sasjs-for-vscode
|
||||||
|
|
||||||
|
github:
|
||||||
|
prebuilds:
|
||||||
|
# enable for the master/default branch (defaults to true)
|
||||||
|
master: true
|
||||||
|
# enable for all branches in this repo (defaults to false)
|
||||||
|
branches: false
|
||||||
|
# enable for pull requests coming from this repo (defaults to true)
|
||||||
|
pullRequests: true
|
||||||
|
# enable for pull requests coming from forks (defaults to false)
|
||||||
|
pullRequestsFromForks: true
|
||||||
|
# add a "Review in Gitpod" button as a comment to pull requests (defaults to true)
|
||||||
|
addComment: true
|
||||||
|
# add a "Review in Gitpod" button to pull requests (defaults to false)
|
||||||
|
addBadge: false
|
||||||
|
# add a label once the prebuild is ready to pull requests (defaults to false)
|
||||||
|
addLabel: prebuilt-in-gitpod
|
||||||
12
.npmignore
Normal file
12
.npmignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
all.sas
|
||||||
|
build.py
|
||||||
|
.gitpod*
|
||||||
|
tests/
|
||||||
|
sasjs/
|
||||||
|
.github/
|
||||||
|
.git-hooks/
|
||||||
|
.vscode/
|
||||||
|
main.dox
|
||||||
|
make_singlefile.sh
|
||||||
|
*.md
|
||||||
|
.all-contributorsrc
|
||||||
13
.sasjslint
Normal file
13
.sasjslint
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"noTrailingSpaces": true,
|
||||||
|
"noEncodedPasswords": true,
|
||||||
|
"hasDoxygenHeader": true,
|
||||||
|
"hasMacroNameInMend": true,
|
||||||
|
"hasMacroParentheses": true,
|
||||||
|
"noNestedMacros": false,
|
||||||
|
"noSpacesInFileNames": true,
|
||||||
|
"maxLineLength": 230,
|
||||||
|
"lowerCaseFileNames": true,
|
||||||
|
"noTabIndentation": true,
|
||||||
|
"indentationMultiple": 2
|
||||||
|
}
|
||||||
9
.vscode/.editorconfig
vendored
Normal file
9
.vscode/.editorconfig
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"search.exclude": {
|
||||||
|
"**/sasjsbuild/**": true,
|
||||||
|
"**/dist/**":true
|
||||||
|
},
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"trim_trailing_whitespace": true
|
||||||
|
}
|
||||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"sasjs.sasjs-for-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.detectIndentation": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.rulers": [
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"files.trimTrailingWhitespace": true
|
||||||
|
}
|
||||||
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
As the changes are managed automatically in github, we don't generate an additional changelog. To view the fixes/features in each release, check out the releases page below:
|
||||||
|
|
||||||
|
[https://github.com/sasjs/core/releases](https://github.com/sasjs/core/releases)
|
||||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
https://sasapps.io/contact-us.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
@@ -27,6 +27,5 @@ To contribute:
|
|||||||
1. Create your feature branch (`git checkout -b myfeature`)
|
1. Create your feature branch (`git checkout -b myfeature`)
|
||||||
2. Make your change
|
2. Make your change
|
||||||
3. Update the `all.sas` file (`python3 build.py`)
|
3. Update the `all.sas` file (`python3 build.py`)
|
||||||
4. Commit the change, using the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0) standard
|
4. Push and make a PR
|
||||||
5. Push and make a PR
|
|
||||||
|
|
||||||
|
|||||||
92
README.md
92
README.md
@@ -1,6 +1,27 @@
|
|||||||
# Macro Core
|
# Macro Core
|
||||||
|
[![npm package][npm-image]][npm-url]
|
||||||
|
[![Github Workflow][githubworkflow-image]][githubworkflow-url]
|
||||||
|
[![Dependency Status][dependency-image]][dependency-url]
|
||||||
|
[]()
|
||||||
|

|
||||||
|
[](/LICENSE)
|
||||||
|

|
||||||
|
[](https://github.com/sasjs/core/issues?q=is%3Aissue+is%3Aclosed)
|
||||||
|
[](https://github.com/sasjs/core/issues)
|
||||||
|

|
||||||
|
[](https://gitpod.io/#https://github.com/sasjs/core)
|
||||||
|
|
||||||
Much quality. Many standards. The **Macro Core** library exists to save time and development effort! Herein ye shall find a veritable host of MIT-licenced, production quality SAS macros. These are a mix of tools, utilities, functions and code generators that are useful in the context of Application Development on the SAS platform (eg https://datacontroller.io). [Contributions](https://github.com/sasjs/core/blob/main/CONTRIBUTING.md) are welcomed.
|
|
||||||
|
[npm-image]:https://img.shields.io/npm/v/@sasjs/core.svg
|
||||||
|
[npm-url]:http://npmjs.org/package/@sasjs/core
|
||||||
|
[githubworkflow-image]:https://github.com/sasjs/core/actions/workflows/main.yml/badge.svg
|
||||||
|
[githubworkflow-url]:https://github.com/sasjs/core/blob/main/.github/workflows/main.yml
|
||||||
|
[dependency-image]:https://david-dm.org/sasjs/core.svg
|
||||||
|
[dependency-url]:https://github.com/sasjs/core/blob/main/package.json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Much quality. Many standards. The **Macro Core** library exists to save time and development effort! Herein ye shall find a veritable host of MIT-licenced, production quality SAS macros. These are a mix of tools, utilities, functions and code generators that are useful in the context of [Application Development](https://sasapps.io) on the SAS platform (eg https://datacontroller.io). [Contributions](https://github.com/sasjs/core/blob/main/CONTRIBUTING.md) are welcomed.
|
||||||
|
|
||||||
You can download and compile them all in just two lines of SAS code:
|
You can download and compile them all in just two lines of SAS code:
|
||||||
|
|
||||||
@@ -9,7 +30,7 @@ filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
|||||||
%inc mc;
|
%inc mc;
|
||||||
```
|
```
|
||||||
|
|
||||||
Documentation: https://sasjs.github.io/core.github.io/files.html
|
Documentation: https://core.sasjs.io
|
||||||
|
|
||||||
# Components
|
# Components
|
||||||
|
|
||||||
@@ -20,6 +41,13 @@ Documentation: https://sasjs.github.io/core.github.io/files.html
|
|||||||
- No X command
|
- No X command
|
||||||
- Prefixes: _mf_, _mp_
|
- Prefixes: _mf_, _mp_
|
||||||
|
|
||||||
|
**fcmp** library (SAS9/Viya)
|
||||||
|
- Function and macro names are identical, except for special cases
|
||||||
|
- Prefixes: _mcf_
|
||||||
|
|
||||||
|
The fcmp macros are used to generate fcmp functions, and can be used with or
|
||||||
|
without the `proc fcmp` wrapper.
|
||||||
|
|
||||||
**meta** library (SAS9 only)
|
**meta** library (SAS9 only)
|
||||||
|
|
||||||
- OS independent
|
- OS independent
|
||||||
@@ -84,7 +112,7 @@ filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
|||||||
## File Properties
|
## File Properties
|
||||||
|
|
||||||
- filenames much match macro names
|
- filenames much match macro names
|
||||||
- filenames must be lowercase
|
- filenames must be lowercase, without spaces
|
||||||
- macro names must be lowercase
|
- macro names must be lowercase
|
||||||
- one macro per file
|
- one macro per file
|
||||||
- prefixes:
|
- prefixes:
|
||||||
@@ -99,7 +127,7 @@ filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
|||||||
- unix style line endings (lf)
|
- unix style line endings (lf)
|
||||||
- individual lines should be no more than 80 characters long
|
- individual lines should be no more than 80 characters long
|
||||||
- UTF-8
|
- UTF-8
|
||||||
- no trailing white space
|
|
||||||
|
|
||||||
## Header Properties
|
## Header Properties
|
||||||
|
|
||||||
@@ -116,19 +144,19 @@ The **Macro Core** documentation is created using [doxygen](http://www.doxygen.n
|
|||||||
All macros must be commented in the doxygen format, to enable the [online documentation](https://core.sasjs.io).
|
All macros must be commented in the doxygen format, to enable the [online documentation](https://core.sasjs.io).
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
SAS code can contain one of two types of dependency - SAS Macros, and SAS Programs. When compiling projects using the [SASjs CLI](https://cli.sasjs.io) the doxygen header is scanned for ` @li` items under the following headers:
|
SAS code can contain one of two types of dependency - SAS Macros, and SAS Includes. When compiling projects using the [SASjs CLI](https://cli.sasjs.io) the doxygen header is scanned for ` @li` items under the following headers:
|
||||||
|
|
||||||
```
|
```sas
|
||||||
<h4> SAS Macros </h4>
|
<h4> SAS Macros </h4>
|
||||||
@li mf_nobs.sas
|
@li mf_nobs.sas
|
||||||
@li mm_assignlib.sas
|
@li mm_assignlib.sas
|
||||||
|
|
||||||
<h4> SAS Programs </h4>
|
<h4> SAS Includes </h4>
|
||||||
@li somefile.ddl SOMEFREF
|
@li somefile.ddl SOMEFREF
|
||||||
@li someprogram.sas FREFTWO
|
@li someprogram.sas FREFTWO
|
||||||
```
|
```
|
||||||
|
|
||||||
The CLI can then extract all the dependencies and insert as precode (SAS Macros) or in a temp engine fileref (SAS Programs) when creating SAS Jobs and Services.
|
The CLI can then extract all the dependencies and insert as precode (SAS Macros) or in a temp engine fileref (SAS Includes) when creating SAS Jobs and Services.
|
||||||
|
|
||||||
When contributing to this library, it is therefore important to ensure that all dependencies are listed in the header in this format.
|
When contributing to this library, it is therefore important to ensure that all dependencies are listed in the header in this format.
|
||||||
|
|
||||||
@@ -136,14 +164,58 @@ When contributing to this library, it is therefore important to ensure that all
|
|||||||
## Coding Standards
|
## Coding Standards
|
||||||
|
|
||||||
- Indentation = 2 spaces. No tabs!
|
- Indentation = 2 spaces. No tabs!
|
||||||
|
- no trailing white space
|
||||||
|
- no invisible characters, other than spaces. If invisibles are needed, use hex literals.
|
||||||
- Macro variables should not have the trailing dot (`&var` not `&var.`) unless necessary to prevent incorrect resolution
|
- Macro variables should not have the trailing dot (`&var` not `&var.`) unless necessary to prevent incorrect resolution
|
||||||
- The closing `%mend;` should not contain the macro name.
|
- The closing `%mend;` should **not** contain the macro name.
|
||||||
- All macros should be defined with brackets, even if no variables are needed - ie `%macro x();` not `%macro x;`
|
- All macros should be defined with brackets, even if no variables are needed - ie `%macro x();` not `%macro x;`
|
||||||
- Mandatory parameters should be positional, all optional parameters should be keyword (var=) style.
|
- Mandatory parameters should be positional, all optional parameters should be keyword (var=) style.
|
||||||
- All dataset references must be 2 level (eg `work.blah`, not `blah`). This is to avoid contention when options [DATASTMTCHK](https://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000279064.htm)=ALLKEYWORDS is in effect.
|
- All dataset references must be 2 level (eg `work.blah`, not `blah`). This is to avoid contention when options [DATASTMTCHK](https://support.sas.com/documentation/cdl/en/lrdict/64316/HTML/default/viewer.htm#a000279064.htm)=ALLKEYWORDS is in effect.
|
||||||
- Avoid naming collisions! All macro variables should be local scope. Use system generated work tables where possible - eg `data ; set sashelp.class; run; data &output; set &syslast; run;`
|
- Avoid naming collisions! All macro variables should be local scope. Use system generated work tables where possible - eg `data ; set sashelp.class; run; data &output; set &syslast; run;`
|
||||||
- If you have a long-running SQL query, the use of a `quit;` statement is recommended in order to benefit from the timing statistics.
|
- The use of `quit;` for `proc sql` is optional unless you are looking to benefit from the timing statistics.
|
||||||
|
|
||||||
# General Notes
|
# General Notes
|
||||||
|
|
||||||
- All macros should be compatible with SAS versions from support level B and above (so currently 9.2 and later). If an earlier version is not supported, then the macro should say as such in the header documentation, and exit gracefully (eg `%if %sysevalf(&sysver<9.3) %then %return`).
|
- All macros should be compatible with SAS versions from support level B and above (so currently 9.2 and later). If an earlier version is not supported, then the macro should say as such in the header documentation, and exit gracefully (eg `%if %sysevalf(&sysver<9.3) %then %return`).
|
||||||
|
|
||||||
|
## Star Gazing
|
||||||
|
|
||||||
|
If you find this library useful, please leave a [star](https://github.com/sasjs/core/stargazers) and help us grow our star graph!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Contributors ✨
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
|
[](#contributors-)
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/allanbowe"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="#business-allanbowe" title="Business development">💼</a> <a href="https://github.com/sasjs/core/commits?author=allanbowe" title="Code">💻</a> <a href="#content-allanbowe" title="Content">🖋</a> <a href="https://github.com/sasjs/core/commits?author=allanbowe" title="Documentation">📖</a> <a href="#infra-allanbowe" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#maintenance-allanbowe" title="Maintenance">🚧</a> <a href="#mentoring-allanbowe" title="Mentoring">🧑🏫</a> <a href="#question-allanbowe" title="Answering Questions">💬</a> <a href="https://github.com/sasjs/core/pulls?q=is%3Apr+reviewed-by%3Aallanbowe" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/sasjs/core/commits?author=allanbowe" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/rafgag"><img src="https://avatars.githubusercontent.com/u/69139928?v=4?s=100" width="100px;" alt=""/><br /><sub><b>rafgag</b></sub></a><br /><a href="https://github.com/sasjs/core/commits?author=rafgag" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/tmoody"><img src="https://avatars.githubusercontent.com/u/79837106?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Trevor Moody</b></sub></a><br /><a href="https://github.com/sasjs/core/commits?author=tmoody" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://krishna-acondy.io/"><img src="https://avatars.githubusercontent.com/u/2980428?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krishna Acondy</b></sub></a><br /><a href="https://github.com/sasjs/core/commits?author=krishna-acondy" title="Code">💻</a> <a href="#infra-krishna-acondy" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#blog-krishna-acondy" title="Blogposts">📝</a> <a href="#content-krishna-acondy" title="Content">🖋</a> <a href="#ideas-krishna-acondy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#video-krishna-acondy" title="Videos">📹</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Saad </b></sub></a><br /><a href="https://github.com/sasjs/core/commits?author=saadjutt01" title="Code">💻</a> <a href="#ideas-saadjutt01" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/core/commits?author=YuryShkoda" title="Code">💻</a> <a href="#infra-YuryShkoda" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#video-YuryShkoda" title="Videos">📹</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="#infra-medjedovicm" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/kkchandok"><img src="https://avatars.githubusercontent.com/u/46090627?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kkchandok</b></sub></a><br /><a href="#ideas-kkchandok" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vladislav Parhomchik</b></sub></a><br /><a href="https://github.com/sasjs/core/commits?author=VladislavParhomchik" title="Tests">⚠️</a> <a href="https://github.com/sasjs/core/pulls?q=is%3Apr+reviewed-by%3AVladislavParhomchik" title="Reviewed Pull Requests">👀</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/vznesh"><img src="https://avatars.githubusercontent.com/u/28916792?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vignesh T.</b></sub></a><br /><a href="https://github.com/sasjs/core/issues?q=author%3Avznesh" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
|||||||
18
SECURITY.md
Normal file
18
SECURITY.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
Security is an extremely high priority when it comes to the SASjs product suite. We take a number of steps across all repositories to minimise risk, such as:
|
||||||
|
|
||||||
|
* Regular dependabot updates
|
||||||
|
* Snyk reports
|
||||||
|
* Minimising dependencies, especially production dependencies (sasjs/core has NONE)
|
||||||
|
* Testing & Code review process
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We support only the latest version
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
We welcome disclosures of all kinds in relation to all the SASjs libraries. You can submit them here: [https://sasapps.io/contact-us](https://sasapps.io/contact-us)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,30 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
@file
|
@file
|
||||||
@brief abort gracefully according to context
|
@brief Abort, ungracefully
|
||||||
@details Do not use directly! See bottom of explanation for details.
|
@details Will abort with a straightforward %abort if the condition is true.
|
||||||
|
|
||||||
Configures an abort mechanism according to site specific policies or the
|
<h4> Related Macros </h4>
|
||||||
particulars of an environment. For instance, can stream custom
|
@li mp_abort.sas
|
||||||
results back to the client in an STP Web App context, or completely stop
|
|
||||||
in the case of a batch run.
|
|
||||||
|
|
||||||
For the sharp eyed readers - this is no longer a macro function!! It became
|
|
||||||
a macro procedure during a project and now it's kinda stuck that way until
|
|
||||||
that project is updated (if it's ever updated). In the meantime we created
|
|
||||||
`mp_abort` which is just a wrapper for this one, and so we recomend you use
|
|
||||||
that for forwards compatibility reasons.
|
|
||||||
|
|
||||||
@param mac= to contain the name of the calling macro
|
|
||||||
@param type= deprecated. Not used.
|
|
||||||
@param msg= message to be returned
|
|
||||||
@param iftrue= supply a condition under which the macro should be executed.
|
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
@cond
|
@cond
|
||||||
**/
|
**/
|
||||||
|
|
||||||
%macro mf_abort(mac=mf_abort.sas, type=, msg=, iftrue=%str(1=1)
|
%macro mf_abort(mac=mf_abort.sas, type=deprecated, msg=, iftrue=%str(1=1)
|
||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
%if not(%eval(%unquote(&iftrue))) %then %return;
|
%if not(%eval(%unquote(&iftrue))) %then %return;
|
||||||
@@ -33,112 +20,8 @@
|
|||||||
%if %length(&mac)>0 %then %put NOTE- called by &mac;
|
%if %length(&mac)>0 %then %put NOTE- called by &mac;
|
||||||
%put NOTE - &msg;
|
%put NOTE - &msg;
|
||||||
|
|
||||||
/* Stored Process Server web app context */
|
%abort;
|
||||||
%if %symexist(_metaperson) or "&SYSPROCESSNAME"="Compute Server" %then %do;
|
|
||||||
options obs=max replace nosyntaxcheck mprint;
|
|
||||||
/* extract log err / warn, if exist */
|
|
||||||
%local logloc logline;
|
|
||||||
%global logmsg; /* capture global messages */
|
|
||||||
%if %symexist(SYSPRINTTOLOG) %then %let logloc=&SYSPRINTTOLOG;
|
|
||||||
%else %let logloc=%qsysfunc(getoption(LOG));
|
|
||||||
proc printto log=log;run;
|
|
||||||
%if %length(&logloc)>0 %then %do;
|
|
||||||
%let logline=0;
|
|
||||||
data _null_;
|
|
||||||
infile &logloc lrecl=5000;
|
|
||||||
input; putlog _infile_;
|
|
||||||
i=1;
|
|
||||||
retain logonce 0;
|
|
||||||
if (_infile_=:"%str(WARN)ING" or _infile_=:"%str(ERR)OR") and logonce=0 then do;
|
|
||||||
call symputx('logline',_n_);
|
|
||||||
logonce+1;
|
|
||||||
end;
|
|
||||||
run;
|
|
||||||
/* capture log including lines BEFORE the err */
|
|
||||||
%if &logline>0 %then %do;
|
|
||||||
data _null_;
|
|
||||||
infile &logloc lrecl=5000;
|
|
||||||
input;
|
|
||||||
i=1;
|
|
||||||
stoploop=0;
|
|
||||||
if _n_ ge &logline-5 and stoploop=0 then do until (i>12);
|
|
||||||
call symputx('logmsg',catx('\n',symget('logmsg'),_infile_));
|
|
||||||
input;
|
|
||||||
i+1;
|
|
||||||
stoploop=1;
|
|
||||||
end;
|
|
||||||
if stoploop=1 then stop;
|
|
||||||
run;
|
|
||||||
%end;
|
|
||||||
%end;
|
|
||||||
|
|
||||||
/* send response in SASjs JSON format */
|
%mend mf_abort;
|
||||||
data _null_;
|
|
||||||
file _webout mod lrecl=32000;
|
|
||||||
length msg $32767;
|
|
||||||
sasdatetime=datetime();
|
|
||||||
msg=cats(symget('msg'),'\n\nLog Extract:\n',symget('logmsg'));
|
|
||||||
/* escape the quotes */
|
|
||||||
msg=tranwrd(msg,'"','\"');
|
|
||||||
/* ditch the CRLFs as chrome complains */
|
|
||||||
msg=compress(msg,,'kw');
|
|
||||||
/* quote without quoting the quotes (which are escaped instead) */
|
|
||||||
msg=cats('"',msg,'"');
|
|
||||||
if symexist('_debug') then debug=symget('_debug');
|
|
||||||
if debug ge 131 then put '>>weboutBEGIN<<';
|
|
||||||
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
|
|
||||||
put ',"sasjsAbort" : [{';
|
|
||||||
put ' "MSG":' msg ;
|
|
||||||
put ' ,"MAC": "' "&mac" '"}]';
|
|
||||||
put ",""SYSUSERID"" : ""&sysuserid"" ";
|
|
||||||
if symexist('_metauser') then do;
|
|
||||||
_METAUSER=quote(trim(symget('_METAUSER')));
|
|
||||||
put ",""_METAUSER"": " _METAUSER;
|
|
||||||
_METAPERSON=quote(trim(symget('_METAPERSON')));
|
|
||||||
put ',"_METAPERSON": ' _METAPERSON;
|
|
||||||
end;
|
|
||||||
_PROGRAM=quote(trim(resolve(symget('_PROGRAM'))));
|
|
||||||
put ',"_PROGRAM" : ' _PROGRAM ;
|
|
||||||
put ",""SYSCC"" : ""&syscc"" ";
|
|
||||||
put ",""SYSERRORTEXT"" : ""&syserrortext"" ";
|
|
||||||
put ",""SYSJOBID"" : ""&sysjobid"" ";
|
|
||||||
put ",""SYSWARNINGTEXT"" : ""&syswarningtext"" ";
|
|
||||||
put ',"END_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '" ';
|
|
||||||
put "}" @;
|
|
||||||
%if &_debug ge 131 %then %do;
|
|
||||||
put '>>weboutEND<<';
|
|
||||||
%end;
|
|
||||||
run;
|
|
||||||
%let syscc=0;
|
|
||||||
%if %symexist(SYS_JES_JOB_URI) %then %do;
|
|
||||||
/* refer web service output to file service in one hit */
|
|
||||||
filename _webout filesrvc parenturi="&SYS_JES_JOB_URI" name="_webout.json";
|
|
||||||
%let rc=%sysfunc(fcopy(_web,_webout));
|
|
||||||
%end;
|
|
||||||
%else %do;
|
|
||||||
data _null_;
|
|
||||||
if symexist('sysprocessmode')
|
|
||||||
then if symget("sysprocessmode")="SAS Stored Process Server"
|
|
||||||
then rc=stpsrvset('program error', 0);
|
|
||||||
run;
|
|
||||||
%end;
|
|
||||||
/**
|
|
||||||
* endsas is reliable but kills some deployments.
|
|
||||||
* Abort variants are ungraceful (non zero return code)
|
|
||||||
* This approach lets SAS run silently until the end :-)
|
|
||||||
*/
|
|
||||||
%put _all_;
|
|
||||||
filename skip temp;
|
|
||||||
data _null_;
|
|
||||||
file skip;
|
|
||||||
put '%macro skip(); %macro skippy();';
|
|
||||||
run;
|
|
||||||
%inc skip;
|
|
||||||
%end;
|
|
||||||
%else %do;
|
|
||||||
%put _all_;
|
|
||||||
%abort cancel;
|
|
||||||
%end;
|
|
||||||
%mend;
|
|
||||||
|
|
||||||
/** @endcond */
|
/** @endcond */
|
||||||
@@ -23,4 +23,4 @@
|
|||||||
%if %sysfunc(exist(&libds)) ne 1 & %sysfunc(exist(&libds,VIEW)) ne 1 %then 0;
|
%if %sysfunc(exist(&libds)) ne 1 & %sysfunc(exist(&libds,VIEW)) ne 1 %then 0;
|
||||||
%else 1;
|
%else 1;
|
||||||
|
|
||||||
%mend;
|
%mend mf_existds;
|
||||||
|
|||||||
@@ -42,6 +42,6 @@
|
|||||||
-1
|
-1
|
||||||
%put &sysmacroname: &feature not found;
|
%put &sysmacroname: &feature not found;
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mf_existfeature;
|
||||||
|
|
||||||
/** @endcond */
|
/** @endcond */
|
||||||
@@ -17,11 +17,17 @@
|
|||||||
%macro mf_existfileref(fref
|
%macro mf_existfileref(fref
|
||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
%if %sysfunc(fileref(&fref))=0 %then %do;
|
%local rc;
|
||||||
|
%let rc=%sysfunc(fileref(&fref));
|
||||||
|
%if &rc=0 %then %do;
|
||||||
|
1
|
||||||
|
%end;
|
||||||
|
%else %if &rc<0 %then %do;
|
||||||
|
%put &sysmacroname: Fileref &fref exists but the underlying file does not;
|
||||||
1
|
1
|
||||||
%end;
|
%end;
|
||||||
%else %do;
|
%else %do;
|
||||||
0
|
0
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mf_existfileref;
|
||||||
37
base/mf_existfunction.sas
Normal file
37
base/mf_existfunction.sas
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Checks if a function exists
|
||||||
|
@details Returns 1 if the function exists, else 0. Note that this function
|
||||||
|
can be slow as it needs to open the sashelp.vfuncs table.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%put %mf_existfunction(CAT);
|
||||||
|
%put %mf_existfunction(DOG);
|
||||||
|
|
||||||
|
Full credit to [Bart](https://sasensei.com/user/305) for the vfunc pointer
|
||||||
|
and the tidy approach for pure macro data set filtering.
|
||||||
|
Check out his [SAS Packages](https://github.com/yabwon/SAS_PACKAGES)
|
||||||
|
framework! Where you can find the same [function](
|
||||||
|
https://github.com/yabwon/SAS_PACKAGES/blob/main/packages/baseplus.md#functionexists-macro
|
||||||
|
).
|
||||||
|
|
||||||
|
@param [in] name (positional) - function name
|
||||||
|
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
/** @cond */
|
||||||
|
%macro mf_existfunction(name
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%local dsid rc exist;
|
||||||
|
%let dsid=%sysfunc(open(sashelp.vfunc(where=(fncname="%upcase(&name)"))));
|
||||||
|
%let exist=1;
|
||||||
|
%let exist=%sysfunc(fetch(&dsid, NOSET));
|
||||||
|
%let rc=%sysfunc(close(&dsid));
|
||||||
|
|
||||||
|
%sysevalf(0 = &exist)
|
||||||
|
|
||||||
|
%mend mf_existfunction;
|
||||||
|
|
||||||
|
/** @endcond */
|
||||||
@@ -30,6 +30,6 @@
|
|||||||
%let rc=%sysfunc(close(&dsid));
|
%let rc=%sysfunc(close(&dsid));
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mf_existvar;
|
||||||
|
|
||||||
/** @endcond */
|
/** @endcond */
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
@details Returns 0 if ANY of the variables do not exist, or 1 if they ALL do.
|
@details Returns 0 if ANY of the variables do not exist, or 1 if they ALL do.
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
%put %mf_existVarList(sashelp.class, age sex name dummyvar)
|
%put %mf_existVarList(sashelp.class, age sex name dummyvar);
|
||||||
|
|
||||||
<h4> SAS Macros </h4>
|
<h4> SAS Macros </h4>
|
||||||
@li mf_abort.sas
|
@li mf_abort.sas
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
%let dsid=%sysfunc(open(&libds,is));
|
%let dsid=%sysfunc(open(&libds,is));
|
||||||
|
|
||||||
%if &dsid=0 %then %do;
|
%if &dsid=0 %then %do;
|
||||||
%put WARNING: unable to open &libds in mf_existvarlist (&dsid);
|
%put %str(WARN)ING: unable to open &libds in mf_existvarlist (&dsid);
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%if %sysfunc(attrn(&dsid,NVARS))=0 %then %do;
|
%if %sysfunc(attrn(&dsid,NVARS))=0 %then %do;
|
||||||
@@ -54,6 +54,6 @@
|
|||||||
0
|
0
|
||||||
%put Vars not found: &found;
|
%put Vars not found: &found;
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mf_existvarlist;
|
||||||
|
|
||||||
/** @endcond */
|
/** @endcond */
|
||||||
73
base/mf_getapploc.sas
Normal file
73
base/mf_getapploc.sas
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Returns the appLoc from the _program variable
|
||||||
|
@details When working with SASjs apps, web services / tests / jobs are always
|
||||||
|
deployed to a root (app) location in the SAS logical folder tree.
|
||||||
|
|
||||||
|
When building apps for use in other environments, you do not necessarily know
|
||||||
|
where the backend services will be deployed. Therefore a function like this
|
||||||
|
is handy in order to dynamically figure out the appLoc, and enable other
|
||||||
|
services to be connected by a relative reference.
|
||||||
|
|
||||||
|
SASjs apps always have the same immediate substructure (one or more of the
|
||||||
|
following):
|
||||||
|
|
||||||
|
@li /data
|
||||||
|
@li /jobs
|
||||||
|
@li /services
|
||||||
|
@li /tests/jobs
|
||||||
|
@li /tests/services
|
||||||
|
@li /tests/macros
|
||||||
|
|
||||||
|
This function works by testing for the existence of any of the above in the
|
||||||
|
automatic _program variable, and returning the part to the left of it.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%put %mf_getapploc(&_program)
|
||||||
|
|
||||||
|
%put %mf_getapploc(/some/location/services/admin/myservice);
|
||||||
|
%put %mf_getapploc(/some/location/jobs/extract/somejob/);
|
||||||
|
%put %mf_getapploc(/some/location/tests/jobs/somejob/);
|
||||||
|
|
||||||
|
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mf_getapploc(pgm);
|
||||||
|
%if "&pgm"="" %then %do;
|
||||||
|
%if %symexist(_program) %then %let pgm=&_program;
|
||||||
|
%else %do;
|
||||||
|
%put &sysmacroname: No value provided and no _program variable available;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%local root;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First check we are not in the tests/macros folder (which has no subfolders)
|
||||||
|
*/
|
||||||
|
%if %index(&pgm,/tests/macros/) %then %do;
|
||||||
|
%let root=%substr(&pgm,1,%index(&pgm,/tests/macros)-1);
|
||||||
|
&root
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next, move up two levels to avoid matches on subfolder or service name
|
||||||
|
*/
|
||||||
|
%let root=%substr(&pgm,1,%length(&pgm)-%length(%scan(&pgm,-1,/))-1);
|
||||||
|
%let root=%substr(&root,1,%length(&root)-%length(%scan(&root,-1,/))-1);
|
||||||
|
|
||||||
|
%if %index(&root,/tests/) %then %do;
|
||||||
|
%let root=%substr(&root,1,%index(&root,/tests/)-1);
|
||||||
|
%end;
|
||||||
|
%else %if %index(&root,/services) %then %do;
|
||||||
|
%let root=%substr(&root,1,%index(&root,/services)-1);
|
||||||
|
%end;
|
||||||
|
%else %if %index(&root,/jobs) %then %do;
|
||||||
|
%let root=%substr(&root,1,%index(&root,/jobs)-1);
|
||||||
|
%end;
|
||||||
|
%else %put &sysmacroname: Could not find an app location from &pgm;
|
||||||
|
&root
|
||||||
|
%mend mf_getapploc ;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
%local dsid rc;
|
%local dsid rc;
|
||||||
%let dsid=%sysfunc(open(&libds,is));
|
%let dsid=%sysfunc(open(&libds,is));
|
||||||
%if &dsid = 0 %then %do;
|
%if &dsid = 0 %then %do;
|
||||||
%put WARNING: Cannot open %trim(&libds), system message below;
|
%put %str(WARN)ING: Cannot open %trim(&libds), system message below;
|
||||||
%put %sysfunc(sysmsg());
|
%put %sysfunc(sysmsg());
|
||||||
-1
|
-1
|
||||||
%end;
|
%end;
|
||||||
@@ -31,4 +31,4 @@
|
|||||||
%sysfunc(attrc(&dsid,&attr))
|
%sysfunc(attrc(&dsid,&attr))
|
||||||
%let rc=%sysfunc(close(&dsid));
|
%let rc=%sysfunc(close(&dsid));
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mf_getattrc;
|
||||||
@@ -31,4 +31,4 @@
|
|||||||
%sysfunc(attrn(&dsid,&attr))
|
%sysfunc(attrn(&dsid,&attr))
|
||||||
%let rc=%sysfunc(close(&dsid));
|
%let rc=%sysfunc(close(&dsid));
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mf_getattrn;
|
||||||
@@ -22,6 +22,9 @@
|
|||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mf_getxengine.sas
|
||||||
|
|
||||||
**/
|
**/
|
||||||
/** @cond */
|
/** @cond */
|
||||||
|
|
||||||
@@ -32,7 +35,9 @@
|
|||||||
/* in case the parameter is a libref.tablename, pull off just the libref */
|
/* in case the parameter is a libref.tablename, pull off just the libref */
|
||||||
%let libref = %upcase(%scan(&libref, 1, %str(.)));
|
%let libref = %upcase(%scan(&libref, 1, %str(.)));
|
||||||
|
|
||||||
%let dsid=%sysfunc(open(sashelp.vlibnam(where=(libname="%upcase(&libref)")),i));
|
%let dsid=%sysfunc(
|
||||||
|
open(sashelp.vlibnam(where=(libname="%upcase(&libref)")),i)
|
||||||
|
);
|
||||||
%if (&dsid ^= 0) %then %do;
|
%if (&dsid ^= 0) %then %do;
|
||||||
%let engnum=%sysfunc(varnum(&dsid,ENGINE));
|
%let engnum=%sysfunc(varnum(&dsid,ENGINE));
|
||||||
%let rc=%sysfunc(fetch(&dsid));
|
%let rc=%sysfunc(fetch(&dsid));
|
||||||
@@ -43,6 +48,6 @@
|
|||||||
|
|
||||||
&engine
|
&engine
|
||||||
|
|
||||||
%mend;
|
%mend mf_getengine;
|
||||||
|
|
||||||
/** @endcond */
|
/** @endcond */
|
||||||
@@ -44,4 +44,4 @@
|
|||||||
%sysfunc(INPUTN(&bytes, best.),sizekmg.)
|
%sysfunc(INPUTN(&bytes, best.),sizekmg.)
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend ;
|
%mend mf_getfilesize ;
|
||||||
@@ -29,4 +29,4 @@
|
|||||||
&valc
|
&valc
|
||||||
%end;
|
%end;
|
||||||
%else %put %str(ERR)OR: Unable to find key &key in ds &libds;
|
%else %put %str(ERR)OR: Unable to find key &key in ds &libds;
|
||||||
%mend;
|
%mend mf_getkeyvalue;
|
||||||
@@ -27,21 +27,23 @@
|
|||||||
or "&sysprocessmode"= "SAS Compute Server" %then %do;
|
or "&sysprocessmode"= "SAS Compute Server" %then %do;
|
||||||
SASVIYA
|
SASVIYA
|
||||||
%end;
|
%end;
|
||||||
%else %if "&sysprocessmode"="SAS Stored Process Server" %then %do;
|
%else %if "&sysprocessmode"="SAS Stored Process Server"
|
||||||
|
or "&sysprocessmode"="SAS Workspace Server"
|
||||||
|
%then %do;
|
||||||
SASMETA
|
SASMETA
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
%else %do;
|
%else %do;
|
||||||
SAS
|
BASESAS
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%else %if %symexist(_metaport) %then %do;
|
%else %if %symexist(_metaport) or %symexist(_metauser) %then %do;
|
||||||
SASMETA
|
SASMETA
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
%else %do;
|
%else %do;
|
||||||
SAS
|
BASESAS
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
@@ -60,4 +62,4 @@
|
|||||||
%else %if &switch=VIYARESTAPI %then %do;
|
%else %if &switch=VIYARESTAPI %then %do;
|
||||||
%mf_trimstr(%sysfunc(getoption(servicesbaseurl)),/)
|
%mf_trimstr(%sysfunc(getoption(servicesbaseurl)),/)
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mf_getplatform;
|
||||||
@@ -50,4 +50,4 @@
|
|||||||
|
|
||||||
&buffer
|
&buffer
|
||||||
|
|
||||||
%mend;
|
%mend mf_getquotedstr;
|
||||||
@@ -38,6 +38,6 @@
|
|||||||
|
|
||||||
&schema
|
&schema
|
||||||
|
|
||||||
%mend;
|
%mend mf_getschema;
|
||||||
|
|
||||||
/** @endcond */
|
/** @endcond */
|
||||||
|
|||||||
@@ -1,37 +1,58 @@
|
|||||||
/**
|
/**
|
||||||
@file
|
@file
|
||||||
@brief Assigns and returns an unused fileref
|
@brief Assigns and returns an unused fileref
|
||||||
@details
|
@details Using the native approach for assigning filerefs fails as some
|
||||||
|
procedures (such as proc http) do not recognise the temporary names (starting
|
||||||
|
with a hash), returning a message such as:
|
||||||
|
|
||||||
|
> ERROR 22-322: Expecting a name.
|
||||||
|
|
||||||
|
This macro works by attempting a random fileref (with a prefix), seeing if it
|
||||||
|
is already assigned, and if not - returning the fileref.
|
||||||
|
|
||||||
|
If your process can accept filerefs with the hash (#) prefix, then set
|
||||||
|
`prefix=0` to revert to the native approach - which is significantly faster
|
||||||
|
when there are a lot of filerefs in a session.
|
||||||
|
|
||||||
Use as follows:
|
Use as follows:
|
||||||
|
|
||||||
%let fileref1=%mf_getuniquefileref();
|
%let fileref1=%mf_getuniquefileref();
|
||||||
%let fileref2=%mf_getuniquefileref();
|
%let fileref2=%mf_getuniquefileref(prefix=0);
|
||||||
%put &fileref1 &fileref2;
|
%put &fileref1 &fileref2;
|
||||||
|
|
||||||
which returns:
|
which returns filerefs similar to:
|
||||||
|
|
||||||
> mcref0 mcref1
|
> _7432233 #LN00070
|
||||||
|
|
||||||
@param prefix= first part of fileref. Remember that filerefs can only be 8
|
@param [in] prefix= (_) first part of fileref. Remember that filerefs can only
|
||||||
characters, so a 7 letter prefix would mean that `maxtries` should be 10.
|
be 8 characters, so a 7 letter prefix would mean `maxtries` should be 10.
|
||||||
@param maxtries= the last part of the libref. Provide an integer value.
|
if using zero (0) as the prefix, a native assignment is used.
|
||||||
|
@param [in] maxtries= (1000) the last part of the libref. Must be an integer.
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
**/
|
**/
|
||||||
|
|
||||||
%macro mf_getuniquefileref(prefix=mcref,maxtries=1000);
|
%macro mf_getuniquefileref(prefix=_,maxtries=1000);
|
||||||
%local x fname;
|
%local rc fname;
|
||||||
%let x=0;
|
%if &prefix=0 %then %do;
|
||||||
%do x=0 %to &maxtries;
|
|
||||||
%if %sysfunc(fileref(&prefix&x)) > 0 %then %do;
|
|
||||||
%let fname=&prefix&x;
|
|
||||||
%let rc=%sysfunc(filename(fname,,temp));
|
%let rc=%sysfunc(filename(fname,,temp));
|
||||||
%if &rc %then %put %sysfunc(sysmsg());
|
%if &rc %then %put %sysfunc(sysmsg());
|
||||||
&prefix&x
|
&fname
|
||||||
%*put &sysmacroname: Fileref &prefix&x was assigned and returned;
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%local x len;
|
||||||
|
%let len=%eval(8-%length(&prefix));
|
||||||
|
%let x=0;
|
||||||
|
%do x=0 %to &maxtries;
|
||||||
|
%let fname=&prefix%substr(%sysfunc(ranuni(0)),3,&len);
|
||||||
|
%if %sysfunc(fileref(&fname)) > 0 %then %do;
|
||||||
|
%let rc=%sysfunc(filename(fname,,temp));
|
||||||
|
%if &rc %then %put %sysfunc(sysmsg());
|
||||||
|
&fname
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%put unable to find available fileref in range &prefix.0-&maxtries;
|
%put unable to find available fileref after &maxtries attempts;
|
||||||
%mend;
|
%end;
|
||||||
|
%mend mf_getuniquefileref;
|
||||||
@@ -37,4 +37,4 @@
|
|||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%put unable to find available libref in range &prefix.0-&maxtries;
|
%put unable to find available libref in range &prefix.0-&maxtries;
|
||||||
%mend;
|
%mend mf_getuniquelibref;
|
||||||
@@ -19,4 +19,4 @@
|
|||||||
|
|
||||||
%macro mf_getuniquename(prefix=MC);
|
%macro mf_getuniquename(prefix=MC);
|
||||||
&prefix.%substr(%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32-%length(&prefix))
|
&prefix.%substr(%sysfunc(compress(%sysfunc(uuidgen()),-)),1,32-%length(&prefix))
|
||||||
%mend;
|
%mend mf_getuniquename;
|
||||||
@@ -39,4 +39,4 @@
|
|||||||
|
|
||||||
%quote(&user)
|
%quote(&user)
|
||||||
|
|
||||||
%mend;
|
%mend mf_getuser;
|
||||||
|
|||||||
@@ -30,4 +30,4 @@
|
|||||||
%trim(&&&variable)
|
%trim(&&&variable)
|
||||||
|
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mf_getvalue;
|
||||||
@@ -29,4 +29,4 @@
|
|||||||
%let rc=%sysfunc(close(&dsid));
|
%let rc=%sysfunc(close(&dsid));
|
||||||
%end;
|
%end;
|
||||||
&nvars
|
&nvars
|
||||||
%mend;
|
%mend mf_getvarcount;
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
8.
|
8.
|
||||||
NOTE: Variable renegade does not exist in test
|
NOTE: Variable renegade does not exist in test
|
||||||
|
|
||||||
@param libds Two part dataset (or view) reference.
|
@param [in] libds Two part dataset (or view) reference.
|
||||||
@param var Variable name for which a format should be returned
|
@param [in] var Variable name for which a format should be returned
|
||||||
@param force Set to 1 to supply a default if the variable has no format
|
@param [in] force=(0) Set to 1 to supply a default if the variable has no format
|
||||||
@returns outputs format
|
@returns outputs format
|
||||||
|
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
@@ -51,7 +51,8 @@
|
|||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%else %do;
|
%else %do;
|
||||||
%put dataset &libds not opened! (rc=&dsid);
|
%put &sysmacroname: dataset &libds not opened! (rc=&dsid);
|
||||||
|
%put &sysmacroname: %sysfunc(sysmsg());
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
%let vlen = %sysfunc(varlen(&dsid, &vnum));
|
%let vlen = %sysfunc(varlen(&dsid, &vnum));
|
||||||
%let vtype = %sysfunc(vartype(&dsid, &vnum.));
|
%let vtype = %sysfunc(vartype(&dsid, &vnum.));
|
||||||
%if &vtype=C %then %let vformat=$&vlen..;
|
%if &vtype=C %then %let vformat=$&vlen..;
|
||||||
%else %let vformat=8.;
|
%else %let vformat=best.;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
|
|
||||||
@@ -68,4 +69,4 @@
|
|||||||
%let rc = %sysfunc(close(&dsid));
|
%let rc = %sysfunc(close(&dsid));
|
||||||
/* Return variable format */
|
/* Return variable format */
|
||||||
&vformat
|
&vformat
|
||||||
%mend;
|
%mend mf_getVarFormat;
|
||||||
@@ -43,10 +43,14 @@
|
|||||||
%let vlen = %str( );
|
%let vlen = %str( );
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%else %put dataset &libds not opened! (rc=&dsid);
|
%else %do;
|
||||||
|
%put &sysmacroname: dataset &libds not opened! (rc=&dsid);
|
||||||
|
%put &sysmacroname: %sysfunc(sysmsg());
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
/* Close dataset */
|
/* Close dataset */
|
||||||
%let rc = %sysfunc(close(&dsid));
|
%let rc = %sysfunc(close(&dsid));
|
||||||
/* Return variable format */
|
/* Return variable format */
|
||||||
&vlen
|
&vlen
|
||||||
%mend;
|
%mend mf_getVarLen;
|
||||||
@@ -10,14 +10,21 @@
|
|||||||
returns:
|
returns:
|
||||||
> List of Variables=Name Sex Age Height Weight
|
> List of Variables=Name Sex Age Height Weight
|
||||||
|
|
||||||
|
For a seperated list of column values:
|
||||||
|
|
||||||
%put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
|
%put %mf_getvarlist(sashelp.class,dlm=%str(,),quote=double);
|
||||||
|
|
||||||
returns:
|
returns:
|
||||||
> "Name","Sex","Age","Height","Weight"
|
> "Name","Sex","Age","Height","Weight"
|
||||||
|
|
||||||
@param libds Two part dataset (or view) reference.
|
@param [in] libds Two part dataset (or view) reference.
|
||||||
@param dlm= provide a delimiter (eg comma or space) to separate the vars
|
@param [in] dlm= ( ) Provide a delimiter (eg comma or space) to separate the
|
||||||
@param quote= use either DOUBLE or SINGLE to quote the results
|
variables
|
||||||
|
@param [in] quote= (none) use either DOUBLE or SINGLE to quote the results
|
||||||
|
@param [in] typefilter= (A) Filter for certain types of column. Valid values:
|
||||||
|
@li A Return All columns
|
||||||
|
@li C Return Character columns
|
||||||
|
@li N Return Numeric columns
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
@@ -27,9 +34,10 @@
|
|||||||
%macro mf_getvarlist(libds
|
%macro mf_getvarlist(libds
|
||||||
,dlm=%str( )
|
,dlm=%str( )
|
||||||
,quote=no
|
,quote=no
|
||||||
|
,typefilter=A
|
||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
/* declare local vars */
|
/* declare local vars */
|
||||||
%local outvar dsid nvars x rc dlm q var;
|
%local outvar dsid nvars x rc dlm q var vtype;
|
||||||
|
|
||||||
/* credit Rowland Hale - byte34 is double quote, 39 is single quote */
|
/* credit Rowland Hale - byte34 is double quote, 39 is single quote */
|
||||||
%if %upcase("e)=DOUBLE %then %let q=%qsysfunc(byte(34));
|
%if %upcase("e)=DOUBLE %then %let q=%qsysfunc(byte(34));
|
||||||
@@ -37,28 +45,30 @@
|
|||||||
/* open dataset in macro */
|
/* open dataset in macro */
|
||||||
%let dsid=%sysfunc(open(&libds));
|
%let dsid=%sysfunc(open(&libds));
|
||||||
|
|
||||||
|
|
||||||
%if &dsid %then %do;
|
%if &dsid %then %do;
|
||||||
%let nvars=%sysfunc(attrn(&dsid,NVARS));
|
%let nvars=%sysfunc(attrn(&dsid,NVARS));
|
||||||
%if &nvars>0 %then %do;
|
%if &nvars>0 %then %do;
|
||||||
/* add first dataset variable to global macro variable */
|
/* add variables with supplied delimeter */
|
||||||
%let outvar=&q.%sysfunc(varname(&dsid,1))&q.;
|
|
||||||
/* add remaining variables with supplied delimeter */
|
|
||||||
%do x=1 %to &nvars;
|
%do x=1 %to &nvars;
|
||||||
|
/* get variable type */
|
||||||
|
%let vtype=%sysfunc(vartype(&dsid,&x));
|
||||||
|
%if &vtype=&typefilter or &typefilter=A %then %do;
|
||||||
%let var=&q.%sysfunc(varname(&dsid,&x))&q.;
|
%let var=&q.%sysfunc(varname(&dsid,&x))&q.;
|
||||||
%if &var=&q&q %then %do;
|
%if &var=&q&q %then %do;
|
||||||
%put &sysmacroname: Empty column found in &libds!;
|
%put &sysmacroname: Empty column found in &libds!;
|
||||||
%let var=&q. &q.;
|
%let var=&q. &q.;
|
||||||
%end;
|
%end;
|
||||||
%if &x=1 %then %let outvar=&var;
|
%if %quote(&outvar)=%quote() %then %let outvar=&var;
|
||||||
%else %let outvar=&outvar.&dlm.&var.;
|
%else %let outvar=&outvar.&dlm.&var.;
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
|
%end;
|
||||||
%let rc=%sysfunc(close(&dsid));
|
%let rc=%sysfunc(close(&dsid));
|
||||||
%end;
|
%end;
|
||||||
%else %do;
|
%else %do;
|
||||||
%put unable to open &libds (rc=&dsid);
|
%put &sysmacroname: Unable to open &libds (rc=&dsid);
|
||||||
|
%put &sysmacroname: SYSMSG= %sysfunc(sysmsg());
|
||||||
%let rc=%sysfunc(close(&dsid));
|
%let rc=%sysfunc(close(&dsid));
|
||||||
%end;
|
%end;
|
||||||
&outvar
|
&outvar
|
||||||
%mend;
|
%mend mf_getvarlist;
|
||||||
@@ -43,7 +43,11 @@ returns:
|
|||||||
%let vnum = %str( );
|
%let vnum = %str( );
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%else %put dataset &ds not opened! (rc=&dsid);
|
%else %do;
|
||||||
|
%put &sysmacroname: dataset &libds not opened! (rc=&dsid);
|
||||||
|
%put &sysmacroname: %sysfunc(sysmsg());
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
/* Close dataset */
|
/* Close dataset */
|
||||||
%let rc = %sysfunc(close(&dsid));
|
%let rc = %sysfunc(close(&dsid));
|
||||||
@@ -51,4 +55,4 @@ returns:
|
|||||||
/* Return variable number */
|
/* Return variable number */
|
||||||
&vnum.
|
&vnum.
|
||||||
|
|
||||||
%mend;
|
%mend mf_getVarNum;
|
||||||
@@ -39,10 +39,14 @@ Usage:
|
|||||||
%let vtype = %str( );
|
%let vtype = %str( );
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%else %put dataset &libds not opened! (rc=&dsid);
|
%else %do;
|
||||||
|
%put &sysmacroname: dataset &libds not opened! (rc=&dsid);
|
||||||
|
%put &sysmacroname: %sysfunc(sysmsg());
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
/* Close dataset */
|
/* Close dataset */
|
||||||
%let rc = %sysfunc(close(&dsid));
|
%let rc = %sysfunc(close(&dsid));
|
||||||
/* Return variable type */
|
/* Return variable type */
|
||||||
&vtype
|
&vtype
|
||||||
%mend;
|
%mend mf_getvartype;
|
||||||
43
base/mf_getxengine.sas
Normal file
43
base/mf_getxengine.sas
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Returns the engine type of a SAS fileref
|
||||||
|
@details Queries sashelp.vextfl to get the xengine value.
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
filename feng temp;
|
||||||
|
%put %mf_getxengine(feng);
|
||||||
|
|
||||||
|
returns:
|
||||||
|
> TEMP
|
||||||
|
|
||||||
|
@param fref The fileref to check
|
||||||
|
|
||||||
|
@returns The XENGINE value in sashelp.vextfl or 0 if not found.
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mf_getengine.sas
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mf_getxengine(fref
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
%local dsid engnum rc engine;
|
||||||
|
|
||||||
|
%let dsid=%sysfunc(
|
||||||
|
open(sashelp.vextfl(where=(fileref="%upcase(&fref)")),i)
|
||||||
|
);
|
||||||
|
%if (&dsid ^= 0) %then %do;
|
||||||
|
%let engnum=%sysfunc(varnum(&dsid,XENGINE));
|
||||||
|
%let rc=%sysfunc(fetch(&dsid));
|
||||||
|
%let engine=%sysfunc(getvarc(&dsid,&engnum));
|
||||||
|
%* put &fref. ENGINE is &engine.;
|
||||||
|
%let rc= %sysfunc(close(&dsid));
|
||||||
|
%end;
|
||||||
|
%else %let engine=0;
|
||||||
|
|
||||||
|
&engine
|
||||||
|
|
||||||
|
%mend mf_getxengine;
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
%put mf_isblank(&var);
|
%put mf_isblank(&var);
|
||||||
|
|
||||||
inspiration: https://support.sas.com/resources/papers/proceedings09/022-2009.pdf
|
inspiration:
|
||||||
|
https://support.sas.com/resources/papers/proceedings09/022-2009.pdf
|
||||||
|
|
||||||
@param param VALUE to be checked
|
@param param VALUE to be checked
|
||||||
|
|
||||||
@@ -23,4 +24,4 @@
|
|||||||
|
|
||||||
%sysevalf(%superq(param)=,boolean)
|
%sysevalf(%superq(param)=,boolean)
|
||||||
|
|
||||||
%mend;
|
%mend mf_isblank;
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
%let isdir=%mf_isdir(/tmp);
|
%let isdir=%mf_isdir(/tmp);
|
||||||
|
|
||||||
With thanks and full credit to Andrea Defronzo - https://www.linkedin.com/in/andrea-defronzo-b1a47460/
|
With thanks and full credit to Andrea Defronzo -
|
||||||
|
https://www.linkedin.com/in/andrea-defronzo-b1a47460/
|
||||||
|
|
||||||
@param path full path of the file/directory to be checked
|
@param path full path of the file/directory to be checked
|
||||||
|
|
||||||
@@ -30,4 +31,4 @@
|
|||||||
|
|
||||||
&is_directory
|
&is_directory
|
||||||
|
|
||||||
%mend;
|
%mend mf_isdir;
|
||||||
@@ -26,4 +26,4 @@
|
|||||||
&root
|
&root
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mf_loc;
|
||||||
|
|||||||
@@ -64,4 +64,4 @@ Usage:
|
|||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
/* exit quietly if directory did exist.*/
|
/* exit quietly if directory did exist.*/
|
||||||
%mend;
|
%mend mf_mkdir;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
@file mf_mval.sas
|
@file mf_mval.sas
|
||||||
@brief Returns a macro variable value if the variable exists
|
@brief Returns a macro variable value if the variable exists
|
||||||
@details Use this macro to avoid repetitive use of `%if %symexist(MACVAR) %then`
|
@details
|
||||||
|
Use this macro to avoid repetitive use of `%if %symexist(MACVAR) %then`
|
||||||
type logic.
|
type logic.
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
@@ -15,4 +16,4 @@
|
|||||||
%if %symexist(&var) %then %do;
|
%if %symexist(&var) %then %do;
|
||||||
%superq(&var)
|
%superq(&var)
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mf_mval;
|
||||||
|
|||||||
@@ -23,4 +23,4 @@
|
|||||||
%macro mf_nobs(libds
|
%macro mf_nobs(libds
|
||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
%mf_getattrn(&libds,NLOBS)
|
%mf_getattrn(&libds,NLOBS)
|
||||||
%mend;
|
%mend mf_nobs;
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
|
|
||||||
|
|
||||||
@param basestr The string to be modified
|
@param basestr The string to be modified
|
||||||
@param trimstr The string to be removed from the end of `basestr`, if it exists
|
@param trimstr The string to be removed from the end of `basestr`, if it
|
||||||
|
exists
|
||||||
|
|
||||||
@return output returns result with the value of `trimstr` removed from the end
|
@return output returns result with the value of `trimstr` removed from the end
|
||||||
|
|
||||||
@@ -46,4 +47,4 @@
|
|||||||
&basestr
|
&basestr
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mf_trimstr;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
@file
|
@file
|
||||||
@brief Creates a Unique ID based on system time in a friendly format
|
@brief Creates a unique ID based on system time in friendly format
|
||||||
@details format = YYYYMMDD_HHMMSSmmm_<sysjobid>_<3randomDigits>
|
@details format = YYYYMMDD_HHMMSSmmm_<sysjobid>_<3randomDigits>
|
||||||
|
|
||||||
%put %mf_uid();
|
%put %mf_uid();
|
||||||
|
|
||||||
@version 9.2
|
@version 9.3
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
|
|
||||||
**/
|
**/
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
%local today now;
|
%local today now;
|
||||||
%let today=%sysfunc(today(),yymmddn8.);
|
%let today=%sysfunc(today(),yymmddn8.);
|
||||||
%let now=%sysfunc(compress(%sysfunc(time(),time12.3),:.));
|
%let now=%sysfunc(compress(%sysfunc(time(),tod12.3),:.));
|
||||||
|
|
||||||
&today._&now._&sysjobid._%sysevalf(%sysfunc(ranuni(0))*999,CEIL)
|
&today._&now._&sysjobid._%sysevalf(%sysfunc(ranuni(0))*999,CEIL)
|
||||||
|
|
||||||
%mend;
|
%mend mf_uid;
|
||||||
@@ -62,4 +62,4 @@
|
|||||||
%else %mf_abort(mac=mf_verifymacvars,type=&mabort,msg=&abortmsg);
|
%else %mf_abort(mac=mf_verifymacvars,type=&mabort,msg=&abortmsg);
|
||||||
%exit_success:
|
%exit_success:
|
||||||
|
|
||||||
%mend;
|
%mend mf_verifymacvars;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
%local count_base count_extr i i2 extr_word base_word match outvar;
|
%local count_base count_extr i i2 extr_word base_word match outvar;
|
||||||
%if %length(&str1)=0 or %length(&str2)=0 %then %do;
|
%if %length(&str1)=0 or %length(&str2)=0 %then %do;
|
||||||
%put WARNING: empty string provided!;
|
%put %str(WARN)ING: empty string provided!;
|
||||||
%put base string (str1)= &str1;
|
%put base string (str1)= &str1;
|
||||||
%put compare string (str2) = &str2;
|
%put compare string (str2) = &str2;
|
||||||
%return;
|
%return;
|
||||||
@@ -50,5 +50,5 @@
|
|||||||
|
|
||||||
&outvar
|
&outvar
|
||||||
|
|
||||||
%mend;
|
%mend mf_wordsInStr1ButNotStr2;
|
||||||
|
|
||||||
|
|||||||
@@ -4,37 +4,106 @@
|
|||||||
@details Configures an abort mechanism according to site specific policies or
|
@details Configures an abort mechanism according to site specific policies or
|
||||||
the particulars of an environment. For instance, can stream custom
|
the particulars of an environment. For instance, can stream custom
|
||||||
results back to the client in an STP Web App context, or completely stop
|
results back to the client in an STP Web App context, or completely stop
|
||||||
in the case of a batch run.
|
in the case of a batch run. For STP sessions
|
||||||
|
|
||||||
|
The method used varies according to the context. Important points:
|
||||||
|
|
||||||
|
@li should not use endsas or abort cancel in 9.4m3 environments as this can
|
||||||
|
cause hung multibridge sessions and result in a frozen STP server
|
||||||
|
@li should not use endsas in viya 3.5 as this destroys the session and cannot
|
||||||
|
fetch results (although both mv_getjoblog.sas and the @sasjs/adapter will
|
||||||
|
recognise this and fetch the log of the parent session instead)
|
||||||
|
@li STP environments must finish cleanly to avoid the log being sent to
|
||||||
|
_webout. To assist with this, we also run stpsrvset('program error', 0)
|
||||||
|
and set SYSCC=0. We take a unique "soft abort" approach - we open a macro
|
||||||
|
but don't close it! This works everywhere EXCEPT inside a \%include inside
|
||||||
|
a macro. For that, we recommend you use mp_include.sas to perform the
|
||||||
|
include, and then call \%mp_abort(mode=INCLUDE) from the source program (ie,
|
||||||
|
OUTSIDE of the top-parent macro).
|
||||||
|
|
||||||
Using SAS Abort Cancel mechanisms can cause hung sessions in some Stored Process
|
|
||||||
environments. This macro takes a unique approach - we set the SAS syscc to 0,
|
|
||||||
run `stpsrvset('program error', 0)` (if SAS 9) and then - we open a macro
|
|
||||||
but don't close it! This provides a graceful abort for SAS web services in all
|
|
||||||
web enabled environments.
|
|
||||||
|
|
||||||
@param mac= to contain the name of the calling macro
|
@param mac= to contain the name of the calling macro
|
||||||
@param msg= message to be returned
|
@param msg= message to be returned
|
||||||
@param iftrue= supply a condition under which the macro should be executed.
|
@param iftrue= supply a condition under which the macro should be executed.
|
||||||
|
@param errds= (work.mp_abort_errds) There is no clean way to end a process
|
||||||
|
within a %include called within a macro. Furthermore, there is no way to
|
||||||
|
test if a macro is called within a %include. To handle this particular
|
||||||
|
scenario, the %include should be switched for the mp_include.sas macro.
|
||||||
|
This provides an indicator that we are running a macro within a \%include
|
||||||
|
(`_SYSINCLUDEFILEDEVICE`) and allows us to provide a dataset with the abort
|
||||||
|
values (msg, mac).
|
||||||
|
We can then run an abort cancel FILE to stop the include running, and pass
|
||||||
|
the dataset back to the calling program to run a regular \%mp_abort().
|
||||||
|
The dataset will contain the following fields:
|
||||||
|
@li iftrue (1=1)
|
||||||
|
@li msg (the message)
|
||||||
|
@li mac (the mac param)
|
||||||
|
|
||||||
@version 9.4M3
|
@param mode= (REGULAR) If mode=INCLUDE then the &errds dataset is checked for
|
||||||
|
an abort status.
|
||||||
|
Valid values:
|
||||||
|
@li REGULAR (default)
|
||||||
|
@li INCLUDE
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_include.sas
|
||||||
|
|
||||||
|
@version 9.4
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
@cond
|
@cond
|
||||||
**/
|
**/
|
||||||
|
|
||||||
%macro mp_abort(mac=mp_abort.sas, type=, msg=, iftrue=%str(1=1)
|
%macro mp_abort(mac=mp_abort.sas, type=, msg=, iftrue=%str(1=1)
|
||||||
|
, errds=work.mp_abort_errds
|
||||||
|
, mode=REGULAR
|
||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%global sysprocessmode sysprocessname;
|
||||||
|
|
||||||
%if not(%eval(%unquote(&iftrue))) %then %return;
|
%if not(%eval(%unquote(&iftrue))) %then %return;
|
||||||
|
|
||||||
%put NOTE: /// mp_abort macro executing //;
|
%put NOTE: /// mp_abort macro executing //;
|
||||||
%if %length(&mac)>0 %then %put NOTE- called by &mac;
|
%if %length(&mac)>0 %then %put NOTE- called by &mac;
|
||||||
%put NOTE - &msg;
|
%put NOTE - &msg;
|
||||||
|
|
||||||
|
%if %symexist(_SYSINCLUDEFILEDEVICE) %then %do;
|
||||||
|
%if "*&_SYSINCLUDEFILEDEVICE*" ne "**" %then %do;
|
||||||
|
data &errds;
|
||||||
|
iftrue='1=1';
|
||||||
|
length mac $100 msg $5000;
|
||||||
|
mac=symget('mac');
|
||||||
|
msg=symget('msg');
|
||||||
|
run;
|
||||||
|
data _null_;
|
||||||
|
abort cancel FILE;
|
||||||
|
run;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
|
||||||
/* Stored Process Server web app context */
|
/* Stored Process Server web app context */
|
||||||
%if %symexist(_metaperson)
|
%if %symexist(_metaperson)
|
||||||
or (%symexist(SYSPROCESSNAME) and "&SYSPROCESSNAME"="Compute Server" )
|
or "&SYSPROCESSNAME "="Compute Server "
|
||||||
|
or &mode=INCLUDE
|
||||||
%then %do;
|
%then %do;
|
||||||
options obs=max replace nosyntaxcheck mprint;
|
options obs=max replace nosyntaxcheck mprint;
|
||||||
|
%if &mode=INCLUDE %then %do;
|
||||||
|
%if %sysfunc(exist(&errds))=1 %then %do;
|
||||||
|
data _null_;
|
||||||
|
set &errds;
|
||||||
|
call symputx('iftrue',iftrue,'l');
|
||||||
|
call symputx('mac',mac,'l');
|
||||||
|
call symputx('msg',msg,'l');
|
||||||
|
putlog (_all_)(=);
|
||||||
|
run;
|
||||||
|
%if (&iftrue)=0 %then %return;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%put &sysmacroname: No include errors found;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
|
||||||
/* extract log errs / warns, if exist */
|
/* extract log errs / warns, if exist */
|
||||||
%local logloc logline;
|
%local logloc logline;
|
||||||
%global logmsg; /* capture global messages */
|
%global logmsg; /* capture global messages */
|
||||||
@@ -48,7 +117,10 @@
|
|||||||
input; putlog _infile_;
|
input; putlog _infile_;
|
||||||
i=1;
|
i=1;
|
||||||
retain logonce 0;
|
retain logonce 0;
|
||||||
if (_infile_=:"%str(WARN)ING" or _infile_=:"%str(ERR)OR") and logonce=0 then do;
|
if (
|
||||||
|
_infile_=:"%str(WARN)ING" or _infile_=:"%str(ERR)OR"
|
||||||
|
) and logonce=0 then
|
||||||
|
do;
|
||||||
call symputx('logline',_n_);
|
call symputx('logline',_n_);
|
||||||
logonce+1;
|
logonce+1;
|
||||||
end;
|
end;
|
||||||
@@ -60,7 +132,7 @@
|
|||||||
input;
|
input;
|
||||||
i=1;
|
i=1;
|
||||||
stoploop=0;
|
stoploop=0;
|
||||||
if _n_ ge &logline-5 and stoploop=0 then do until (i>12);
|
if _n_ ge &logline-15 and stoploop=0 then do until (i>22);
|
||||||
call symputx('logmsg',catx('\n',symget('logmsg'),_infile_));
|
call symputx('logmsg',catx('\n',symget('logmsg'),_infile_));
|
||||||
input;
|
input;
|
||||||
i+1;
|
i+1;
|
||||||
@@ -85,8 +157,8 @@
|
|||||||
|
|
||||||
/* send response in SASjs JSON format */
|
/* send response in SASjs JSON format */
|
||||||
data _null_;
|
data _null_;
|
||||||
file _webout mod lrecl=32000;
|
file _webout mod lrecl=32000 encoding='utf-8';
|
||||||
length msg $32767 debug $8;
|
length msg $32767 ;
|
||||||
sasdatetime=datetime();
|
sasdatetime=datetime();
|
||||||
msg=cats(symget('msg'),'\n\nLog Extract:\n',symget('logmsg'));
|
msg=cats(symget('msg'),'\n\nLog Extract:\n',symget('logmsg'));
|
||||||
/* escape the quotes */
|
/* escape the quotes */
|
||||||
@@ -97,7 +169,7 @@
|
|||||||
msg=cats('"',msg,'"');
|
msg=cats('"',msg,'"');
|
||||||
if symexist('_debug') then debug=quote(trim(symget('_debug')));
|
if symexist('_debug') then debug=quote(trim(symget('_debug')));
|
||||||
else debug='""';
|
else debug='""';
|
||||||
if debug ge '"131"' then put '>>weboutBEGIN<<';
|
put '>>weboutBEGIN<<';
|
||||||
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
|
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
|
||||||
put ',"sasjsAbort" : [{';
|
put ',"sasjsAbort" : [{';
|
||||||
put ' "MSG":' msg ;
|
put ' "MSG":' msg ;
|
||||||
@@ -117,39 +189,68 @@
|
|||||||
_PROGRAM=quote(trim(resolve(symget('_PROGRAM'))));
|
_PROGRAM=quote(trim(resolve(symget('_PROGRAM'))));
|
||||||
put ',"_PROGRAM" : ' _PROGRAM ;
|
put ',"_PROGRAM" : ' _PROGRAM ;
|
||||||
put ",""SYSCC"" : ""&syscc"" ";
|
put ",""SYSCC"" : ""&syscc"" ";
|
||||||
put ",""SYSERRORTEXT"" : ""&syserrortext"" ";
|
syserrortext=quote(trim(symget('syserrortext')));
|
||||||
|
put ",""SYSERRORTEXT"" : " syserrortext;
|
||||||
|
put ",""SYSHOSTNAME"" : ""&syshostname"" ";
|
||||||
put ",""SYSJOBID"" : ""&sysjobid"" ";
|
put ",""SYSJOBID"" : ""&sysjobid"" ";
|
||||||
put ",""SYSWARNINGTEXT"" : ""&syswarningtext"" ";
|
put ",""SYSSCPL"" : ""&sysscpl"" ";
|
||||||
|
put ",""SYSSITE"" : ""&syssite"" ";
|
||||||
|
sysvlong=quote(trim(symget('sysvlong')));
|
||||||
|
put ',"SYSVLONG" : ' sysvlong;
|
||||||
|
syswarningtext=quote(trim(symget('syswarningtext')));
|
||||||
|
put ",""SYSWARNINGTEXT"" : " syswarningtext;
|
||||||
put ',"END_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '" ';
|
put ',"END_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '" ';
|
||||||
put "}" @;
|
put "}" @;
|
||||||
if debug ge '"131"' then put '>>weboutEND<<';
|
put '>>weboutEND<<';
|
||||||
run;
|
run;
|
||||||
|
|
||||||
%let syscc=0;
|
%put _all_;
|
||||||
%if %symexist(_metaport) %then %do;
|
|
||||||
|
%if "&sysprocessmode " = "SAS Stored Process Server " %then %do;
|
||||||
data _null_;
|
data _null_;
|
||||||
if symexist('sysprocessmode')
|
putlog 'stpsrvset program err and syscc';
|
||||||
then if symget("sysprocessmode")="SAS Stored Process Server"
|
rc=stpsrvset('program error', 0);
|
||||||
then rc=stpsrvset('program error', 0);
|
call symputx("syscc",0,"g");
|
||||||
run;
|
run;
|
||||||
%end;
|
|
||||||
/**
|
/**
|
||||||
* endsas is reliable but kills some deployments.
|
* endsas kills 9.4m3 deployments by orphaning multibridges.
|
||||||
* Abort variants are ungraceful (non zero return code)
|
* Abort variants are ungraceful (non zero return code)
|
||||||
* This approach lets SAS run silently until the end :-)
|
* This approach lets SAS run silently until the end :-)
|
||||||
|
* Caution - fails when called within a %include within a macro
|
||||||
|
* Use mp_include() to handle this.
|
||||||
*/
|
*/
|
||||||
%put _all_;
|
|
||||||
filename skip temp;
|
filename skip temp;
|
||||||
data _null_;
|
data _null_;
|
||||||
file skip;
|
file skip;
|
||||||
put '%macro skip(); %macro skippy();';
|
put '%macro skip();';
|
||||||
|
comment '%mend skip; -> fix lint ';
|
||||||
|
put '%macro skippy();';
|
||||||
|
comment '%mend skippy; -> fix lint ';
|
||||||
run;
|
run;
|
||||||
%inc skip;
|
%inc skip;
|
||||||
%end;
|
%end;
|
||||||
|
%else %if "&sysprocessmode " = "SAS Compute Server " %then %do;
|
||||||
|
/* endsas kills the session making it harder to fetch results */
|
||||||
|
data _null_;
|
||||||
|
syswarningtext=symget('syswarningtext');
|
||||||
|
syserrortext=symget('syserrortext');
|
||||||
|
abort_msg=symget('msg');
|
||||||
|
syscc=symget('syscc');
|
||||||
|
sysuserid=symget('sysuserid');
|
||||||
|
iftrue=symget('iftrue');
|
||||||
|
put (_all_)(/=);
|
||||||
|
call symputx('syscc',0);
|
||||||
|
abort cancel nolist;
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%abort cancel;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
%else %do;
|
%else %do;
|
||||||
%put _all_;
|
%put _all_;
|
||||||
%abort cancel;
|
%abort cancel;
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mp_abort;
|
||||||
|
|
||||||
/** @endcond */
|
/** @endcond */
|
||||||
57
base/mp_appendfile.sas
Normal file
57
base/mp_appendfile.sas
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Append (concatenate) two or more files.
|
||||||
|
@details Will append one more more `appendrefs` (filerefs) to a `baseref`.
|
||||||
|
Uses a binary mechanism, so will work with any file type. For that reason -
|
||||||
|
use with care! And supply your own trailing carriage returns in each file..
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
filename tmp1 temp;
|
||||||
|
filename tmp2 temp;
|
||||||
|
filename tmp3 temp;
|
||||||
|
data _null_; file tmp1; put 'base file';
|
||||||
|
data _null_; file tmp2; put 'append1';
|
||||||
|
data _null_; file tmp3; put 'append2';
|
||||||
|
run;
|
||||||
|
%mp_appendfile(baseref=tmp1, appendrefs=tmp2 tmp3)
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] baseref= Fileref of the base file (should exist)
|
||||||
|
@param [in] appendrefs= One or more filerefs to be appended to the base
|
||||||
|
fileref. Space separated.
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe, source: https://github.com/sasjs/core
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mp_binarycopy.sas
|
||||||
|
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_appendfile(
|
||||||
|
baseref=0,
|
||||||
|
appendrefs=0
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&baseref=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Baseref NOT specified!)
|
||||||
|
)
|
||||||
|
%mp_abort(iftrue= (&appendrefs=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Appendrefs NOT specified!)
|
||||||
|
)
|
||||||
|
|
||||||
|
%local i;
|
||||||
|
%do i=1 %to %sysfunc(countw(&appendrefs));
|
||||||
|
%mp_abort(iftrue= (&syscc>0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc)
|
||||||
|
)
|
||||||
|
%mp_binarycopy(inref=%scan(&appendrefs,&i), outref=&baseref, mode=APPEND)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%mend mp_appendfile;
|
||||||
56
base/mp_assert.sas
Normal file
56
base/mp_assert.sas
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Generic assertion
|
||||||
|
@details Useful in the context of writing sasjs tests. The results of the
|
||||||
|
test are _appended_ to the &outds. table.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
%mp_assert(iftrue=(1=1),
|
||||||
|
desc=Obviously true
|
||||||
|
)
|
||||||
|
|
||||||
|
%mp_assert(iftrue=(1=0),
|
||||||
|
desc=Will fail
|
||||||
|
)
|
||||||
|
|
||||||
|
@param [in] iftrue= (1=1) A condition where, if true, the test is a PASS.
|
||||||
|
Else, the test is a fail.
|
||||||
|
|
||||||
|
@param [in] desc= (Testing observations) The user provided test description
|
||||||
|
@param [out] outds= (work.test_results) The output dataset to contain the
|
||||||
|
results. If it does not exist, it will be created, with the following format:
|
||||||
|
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|
||||||
|
|---|---|---|
|
||||||
|
|User Provided description|PASS|Column &inds contained ALL columns|
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_assert(iftrue=(1=1),
|
||||||
|
desc=0,
|
||||||
|
outds=work.test_results
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
data ;
|
||||||
|
length test_description $256 test_result $4 test_comments $256;
|
||||||
|
test_description=symget('desc');
|
||||||
|
test_comments="&sysmacroname: Test result of "!!symget('iftrue');
|
||||||
|
%if %eval(%unquote(&iftrue)) %then %do;
|
||||||
|
test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
test_result='FAIL';
|
||||||
|
%end;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%local ds ;
|
||||||
|
%let ds=&syslast;
|
||||||
|
proc append base=&outds data=&ds;
|
||||||
|
run;
|
||||||
|
proc sql;
|
||||||
|
drop table &ds;
|
||||||
|
|
||||||
|
%mend mp_assert;
|
||||||
145
base/mp_assertcols.sas
Normal file
145
base/mp_assertcols.sas
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Asserts the existence (or not) of columns
|
||||||
|
@details Useful in the context of writing sasjs tests. The results of the
|
||||||
|
test are _appended_ to the &outds. table.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
%mp_assertcols(sashelp.class,
|
||||||
|
cols=name age sex,
|
||||||
|
test=ALL,
|
||||||
|
desc=check all columns exist
|
||||||
|
)
|
||||||
|
|
||||||
|
%mp_assertcols(sashelp.class,
|
||||||
|
cols=a b c,
|
||||||
|
test=NONE
|
||||||
|
)
|
||||||
|
|
||||||
|
%mp_assertcols(sashelp.class,
|
||||||
|
cols=age depth,
|
||||||
|
test=ANY
|
||||||
|
)
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_existds.sas
|
||||||
|
@li mf_existvarlist.sas
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mf_wordsinstr1butnotstr2.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] inds The input library.dataset to test for values
|
||||||
|
@param [in] cols= The list of columns to check for
|
||||||
|
@param [in] desc= (Testing observations) The user provided test description
|
||||||
|
@param [in] test= (ALL) The test to apply. Valid values are:
|
||||||
|
@li ALL - Test is a PASS if ALL columns exist in &inds
|
||||||
|
@li ANY - Test is a PASS if ANY of the columns exist in &inds
|
||||||
|
@li NONE - Test is a PASS if NONE of the columns exist in &inds
|
||||||
|
@param [out] outds= (work.test_results) The output dataset to contain the
|
||||||
|
results. If it does not exist, it will be created, with the following format:
|
||||||
|
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|
||||||
|
|---|---|---|
|
||||||
|
|User Provided description|PASS|Column &inds contained ALL columns|
|
||||||
|
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_assertdsobs.sas
|
||||||
|
@li mp_assertcolvals.sas
|
||||||
|
@li mp_assertdsobs.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_assertcols(inds,
|
||||||
|
cols=0,
|
||||||
|
test=ALL,
|
||||||
|
desc=0,
|
||||||
|
outds=work.test_results
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
%local lib ds ;
|
||||||
|
%let lib=%scan(&inds,1,%str(.));
|
||||||
|
%let ds=%scan(&inds,2,%str(.));
|
||||||
|
%let cols=%upcase(&cols);
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (%mf_existds(&lib..&ds)=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&lib..&ds not found!)
|
||||||
|
)
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&cols=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(No cols provided)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
%let test=%upcase(&test);
|
||||||
|
|
||||||
|
%if &test ne ANY and &test ne ALL and &test ne NONE %then %do;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Invalid test - &test)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* now do the actual test!
|
||||||
|
*/
|
||||||
|
%local result;
|
||||||
|
%if %mf_existVarList(&inds,&cols)=1 %then %let result=ALL;
|
||||||
|
%else %do;
|
||||||
|
%local targetcols compare;
|
||||||
|
%let targetcols=%upcase(%mf_getvarlist(&inds));
|
||||||
|
%let compare=%mf_wordsinstr1butnotstr2(
|
||||||
|
Str1=&cols,
|
||||||
|
Str2=&targetcols
|
||||||
|
);
|
||||||
|
%if %cmpres(&compare)=%cmpres(&cols) %then %let result=NONE;
|
||||||
|
%else %let result=SOME;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
data;
|
||||||
|
length test_description $256 test_result $4 test_comments $256;
|
||||||
|
test_description=symget('desc');
|
||||||
|
if test_description='0'
|
||||||
|
then test_description="Testing &inds for existence of &test of: &cols";
|
||||||
|
|
||||||
|
test_result='FAIL';
|
||||||
|
test_comments="&sysmacroname: &inds has &result columns ";
|
||||||
|
%if &test=ALL %then %do;
|
||||||
|
%if &result=ALL %then %do;
|
||||||
|
test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%else %if &test=ANY %then %do;
|
||||||
|
%if &result=SOME %then %do;
|
||||||
|
test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%else %if &test=NONE %then %do;
|
||||||
|
%if &result=NONE %then %do;
|
||||||
|
test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
test_comments="&sysmacroname: Unsatisfied test condition - &test";
|
||||||
|
%end;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%local ds;
|
||||||
|
%let ds=&syslast;
|
||||||
|
proc append base=&outds data=&ds;
|
||||||
|
run;
|
||||||
|
proc sql;
|
||||||
|
drop table &ds;
|
||||||
|
|
||||||
|
%mend mp_assertcols;
|
||||||
168
base/mp_assertcolvals.sas
Normal file
168
base/mp_assertcolvals.sas
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Asserts the values in a column
|
||||||
|
@details Useful in the context of writing sasjs tests. The results of the
|
||||||
|
test are _appended_ to the &outds. table.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
data work.checkds;
|
||||||
|
do checkval='Jane','James','Jill';
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%mp_assertcolvals(sashelp.class.name,
|
||||||
|
checkvals=work.checkds.checkval,
|
||||||
|
desc=At least one value has a match,
|
||||||
|
test=ANYVAL
|
||||||
|
)
|
||||||
|
|
||||||
|
data work.check;
|
||||||
|
do val='M','F';
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%mp_assertcolvals(sashelp.class.sex,
|
||||||
|
checkvals=work.check.val,
|
||||||
|
desc=All values have a match,
|
||||||
|
test=ALLVALS
|
||||||
|
)
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_existds.sas
|
||||||
|
@li mf_getuniquename.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] indscol The input library.dataset.column to test for values
|
||||||
|
@param [in] checkvals= A library.dataset.column value containing a UNIQUE
|
||||||
|
list of values to be compared against the source (indscol).
|
||||||
|
@param [in] desc= (Testing observations) The user provided test description
|
||||||
|
@param [in] test= (ALLVALS) The test to apply. Valid values are:
|
||||||
|
@li ALLVALS - Test is a PASS if ALL values have a match in checkvals
|
||||||
|
@li ANYVAL - Test is a PASS if at least 1 value has a match in checkvals
|
||||||
|
@param [out] outds= (work.test_results) The output dataset to contain the
|
||||||
|
results. If it does not exist, it will be created, with the following format:
|
||||||
|
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|
||||||
|
|---|---|---|
|
||||||
|
|User Provided description|PASS|Column &indscol contained ALL target vals|
|
||||||
|
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_assertdsobs.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_assertcolvals(indscol,
|
||||||
|
checkvals=0,
|
||||||
|
test=ALLVALS,
|
||||||
|
desc=mp_assertcolvals - no desc provided,
|
||||||
|
outds=work.test_results
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
%local lib ds col clib cds ccol nobs;
|
||||||
|
%let lib=%scan(&indscol,1,%str(.));
|
||||||
|
%let ds=%scan(&indscol,2,%str(.));
|
||||||
|
%let col=%scan(&indscol,3,%str(.));
|
||||||
|
%mp_abort(iftrue= (%mf_existds(&lib..&ds)=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&lib..&ds not found!)
|
||||||
|
)
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&checkvals=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Set CHECKVALS to a library.dataset.column containing check vals)
|
||||||
|
)
|
||||||
|
%let clib=%scan(&checkvals,1,%str(.));
|
||||||
|
%let cds=%scan(&checkvals,2,%str(.));
|
||||||
|
%let ccol=%scan(&checkvals,3,%str(.));
|
||||||
|
%mp_abort(iftrue= (%mf_existds(&clib..&cds)=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&clib..&cds not found!)
|
||||||
|
)
|
||||||
|
%let nobs=%mf_nobs(&clib..&cds);
|
||||||
|
%mp_abort(iftrue= (&nobs=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(&clib..&cds is empty!)
|
||||||
|
)
|
||||||
|
|
||||||
|
%let test=%upcase(&test);
|
||||||
|
|
||||||
|
%if &test ne ALLVALS and &test ne ANYVAL %then %do;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Invalid test - &test)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%local result orig;
|
||||||
|
%let result=-1;
|
||||||
|
%let orig=-1;
|
||||||
|
proc sql noprint;
|
||||||
|
select count(*) into: result
|
||||||
|
from &lib..&ds
|
||||||
|
where &col not in (
|
||||||
|
select &ccol from &clib..&cds
|
||||||
|
);
|
||||||
|
select count(*) into: orig from &lib..&ds;
|
||||||
|
quit;
|
||||||
|
|
||||||
|
%local notfound tmp1 tmp2;
|
||||||
|
%let tmp1=%mf_getuniquename();
|
||||||
|
%let tmp2=%mf_getuniquename();
|
||||||
|
|
||||||
|
/* this is a bit convoluted - but using sql outobs=10 throws warnings */
|
||||||
|
proc sql noprint;
|
||||||
|
create view &tmp1 as
|
||||||
|
select distinct &col
|
||||||
|
from &lib..&ds
|
||||||
|
where &col not in (
|
||||||
|
select &ccol from &clib..&cds
|
||||||
|
);
|
||||||
|
data &tmp2;
|
||||||
|
set &tmp1;
|
||||||
|
if _n_>10 then stop;
|
||||||
|
run;
|
||||||
|
proc sql;
|
||||||
|
select distinct &col into: notfound separated by ' ' from &tmp2;
|
||||||
|
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc after macro query)
|
||||||
|
)
|
||||||
|
|
||||||
|
data;
|
||||||
|
length test_description $256 test_result $4 test_comments $256;
|
||||||
|
test_description=symget('desc');
|
||||||
|
test_result='FAIL';
|
||||||
|
test_comments="&sysmacroname: &lib..&ds..&col has &result values "
|
||||||
|
!!"not in &clib..&cds..&ccol.. First 10 vals:"!!symget('notfound');
|
||||||
|
%if &test=ANYVAL %then %do;
|
||||||
|
if &result < &orig then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=ALLVALS %then %do;
|
||||||
|
if &result=0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
test_comments="&sysmacroname: Unsatisfied test condition - &test";
|
||||||
|
%end;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%local ds;
|
||||||
|
%let ds=&syslast;
|
||||||
|
proc append base=&outds data=&ds;
|
||||||
|
run;
|
||||||
|
proc sql;
|
||||||
|
drop table &ds;
|
||||||
|
|
||||||
|
%mend mp_assertcolvals;
|
||||||
118
base/mp_assertdsobs.sas
Normal file
118
base/mp_assertdsobs.sas
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Asserts the number of observations in a dataset
|
||||||
|
@details Useful in the context of writing sasjs tests. The results of the
|
||||||
|
test are _appended_ to the &outds. table.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
%mp_assertdsobs(sashelp.class) %* tests if any observations are present;
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_nobs.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] inds input dataset to test for presence of observations
|
||||||
|
@param [in] desc= (Testing observations) The user provided test description
|
||||||
|
@param [in] test= (HASOBS) The test to apply. Valid values are:
|
||||||
|
@li HASOBS - Test is a PASS if the input dataset has any observations
|
||||||
|
@li EMPTY - Test is a PASS if input dataset is empty
|
||||||
|
@li EQUALS [integer] - Test passes if row count matches the provided integer
|
||||||
|
@LI ATLEAST [integer] - Test passes if row count is more than or equal to
|
||||||
|
the provided integer
|
||||||
|
@LI ATMOST [integer] - Test passes if row count is less than or equal to
|
||||||
|
the provided integer
|
||||||
|
@param [out] outds= (work.test_results) The output dataset to contain the
|
||||||
|
results. If it does not exist, it will be created, with the following format:
|
||||||
|
|TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
|
||||||
|
|---|---|---|
|
||||||
|
|User Provided description|PASS|Dataset &inds has XX obs|
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_assertcolvals.sas
|
||||||
|
@li mp_assert.sas
|
||||||
|
@li mp_assertcols.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_assertdsobs(inds,
|
||||||
|
test=HASOBS,
|
||||||
|
desc=Testing observations,
|
||||||
|
outds=work.test_results
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%local nobs;
|
||||||
|
%let nobs=%mf_nobs(&inds);
|
||||||
|
%let test=%upcase(&test);
|
||||||
|
|
||||||
|
%if %substr(&test.xxxxx,1,6)=EQUALS %then %do;
|
||||||
|
%let val=%scan(&test,2,%str( ));
|
||||||
|
%mp_abort(iftrue= (%DATATYP(&val)=CHAR)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Invalid test - &test, expected EQUALS [integer])
|
||||||
|
)
|
||||||
|
%let test=EQUALS;
|
||||||
|
%end;
|
||||||
|
%else %if %substr(&test.xxxxxxx,1,7)=ATLEAST %then %do;
|
||||||
|
%let val=%scan(&test,2,%str( ));
|
||||||
|
%mp_abort(iftrue= (%DATATYP(&val)=CHAR)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Invalid test - &test, expected ATLEAST [integer])
|
||||||
|
)
|
||||||
|
%let test=ATLEAST;
|
||||||
|
%end;
|
||||||
|
%else %if %substr(&test.xxxxxxx,1,7)=ATMOST %then %do;
|
||||||
|
%let val=%scan(&test,2,%str( ));
|
||||||
|
%mp_abort(iftrue= (%DATATYP(&val)=CHAR)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Invalid test - &test, expected ATMOST [integer])
|
||||||
|
)
|
||||||
|
%let test=ATMOST;
|
||||||
|
%end;
|
||||||
|
%else %if &test ne HASOBS and &test ne EMPTY %then %do;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Invalid test - &test)
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
data;
|
||||||
|
length test_description $256 test_result $4 test_comments $256;
|
||||||
|
test_description=symget('desc');
|
||||||
|
test_result='FAIL';
|
||||||
|
test_comments="&sysmacroname: Dataset &inds has &nobs observations.";
|
||||||
|
test_comments=test_comments!!" Test was "!!symget('test');
|
||||||
|
%if &test=HASOBS %then %do;
|
||||||
|
if &nobs>0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=EMPTY %then %do;
|
||||||
|
if &nobs=0 then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=EQUALS %then %do;
|
||||||
|
if &nobs=&val then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=ATLEAST %then %do;
|
||||||
|
if &nobs ge &val then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %if &test=ATMOST %then %do;
|
||||||
|
if &nobs le &val then test_result='PASS';
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
test_comments="&sysmacroname: Unsatisfied test condition - &test";
|
||||||
|
%end;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%local ds;
|
||||||
|
%let ds=&syslast;
|
||||||
|
|
||||||
|
proc append base=&outds data=&ds;
|
||||||
|
run;
|
||||||
|
|
||||||
|
proc sql;
|
||||||
|
drop table &ds;
|
||||||
|
|
||||||
|
%mend mp_assertdsobs;
|
||||||
117
base/mp_base64copy.sas
Normal file
117
base/mp_base64copy.sas
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Convert a file to/from base64 format
|
||||||
|
@details Creates a new version of a file either encoded or decoded using
|
||||||
|
Base64. Inspired by this post by Michael Dixon:
|
||||||
|
https://support.selerity.com.au/hc/en-us/articles/223345708-Tip-SAS-and-Base64
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
filename tmp temp;
|
||||||
|
data _null_;
|
||||||
|
file tmp;
|
||||||
|
put 'base ik ally';
|
||||||
|
run;
|
||||||
|
%mp_base64copy(inref=tmp, outref=myref, action=ENCODE)
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
infile myref;
|
||||||
|
input;
|
||||||
|
put _infile_;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_base64copy(inref=myref, outref=mynewref, action=DECODE)
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
infile mynewref;
|
||||||
|
input;
|
||||||
|
put _infile_;
|
||||||
|
run;
|
||||||
|
|
||||||
|
@param [in] inref= Fileref of the input file (should exist)
|
||||||
|
@param [out] outref= Output fileref. If it does not exist, it is created.
|
||||||
|
@param [in] action= (ENCODE) The action to take. Valid values:
|
||||||
|
@li ENCODE - Convert the file to base64 format
|
||||||
|
@li DECODE - Decode the file from base64 format
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe, source: https://github.com/sasjs/core
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_base64copy(
|
||||||
|
inref=0,
|
||||||
|
outref=0,
|
||||||
|
action=ENCODE
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%let inref=%upcase(&inref);
|
||||||
|
%let outref=%upcase(&outref);
|
||||||
|
%let action=%upcase(&action);
|
||||||
|
%local infound outfound;
|
||||||
|
%let infound=0;
|
||||||
|
%let outfound=0;
|
||||||
|
data _null_;
|
||||||
|
set sashelp.vextfl(where=(fileref="&inref" or fileref="&outref"));
|
||||||
|
if fileref="&inref" then call symputx('infound',1,'l');
|
||||||
|
if fileref="&outref" then call symputx('outfound',1,'l');
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&infound=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(INREF &inref NOT FOUND!)
|
||||||
|
)
|
||||||
|
%mp_abort(iftrue= (&outref=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(OUTREF NOT PROVIDED!)
|
||||||
|
)
|
||||||
|
%mp_abort(iftrue= (&action ne ENCODE and &action ne DECODE)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Invalid action! Should be ENCODE OR DECODE)
|
||||||
|
)
|
||||||
|
|
||||||
|
%if &outfound=0 %then %do;
|
||||||
|
filename &outref temp lrecl=2097088;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%if &action=ENCODE %then %do;
|
||||||
|
data _null_;
|
||||||
|
length b64 $ 76 line $ 57;
|
||||||
|
retain line "";
|
||||||
|
infile &inref recfm=F lrecl= 1 end=eof;
|
||||||
|
input @1 stream $char1.;
|
||||||
|
file &outref recfm=N;
|
||||||
|
substr(line,(_N_-(CEIL(_N_/57)-1)*57),1) = byte(rank(stream));
|
||||||
|
if mod(_N_,57)=0 or EOF then do;
|
||||||
|
if eof then b64=put(trim(line),$base64X76.);
|
||||||
|
else b64=put(line, $base64X76.);
|
||||||
|
put b64 + (-1) @;
|
||||||
|
line="";
|
||||||
|
end;
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
%else %if &action=DECODE %then %do;
|
||||||
|
data _null_;
|
||||||
|
length filein 8 fileout 8;
|
||||||
|
filein = fopen("&inref",'I',4,'B');
|
||||||
|
fileout = fopen("&outref",'O',3,'B');
|
||||||
|
char= '20'x;
|
||||||
|
do while(fread(filein)=0);
|
||||||
|
length raw $4;
|
||||||
|
do i=1 to 4;
|
||||||
|
rc=fget(filein,char,1);
|
||||||
|
substr(raw,i,1)=char;
|
||||||
|
end;
|
||||||
|
rc = fput(fileout,input(raw,$base64X4.));
|
||||||
|
rc = fwrite(fileout);
|
||||||
|
end;
|
||||||
|
rc = fclose(filein);
|
||||||
|
rc = fclose(fileout);
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%mend mp_base64copy;
|
||||||
@@ -4,14 +4,34 @@
|
|||||||
@details Reads in a file byte by byte and writes it back out. Is an
|
@details Reads in a file byte by byte and writes it back out. Is an
|
||||||
os-independent method to copy files. In case of naming collision, the
|
os-independent method to copy files. In case of naming collision, the
|
||||||
default filerefs can be modified.
|
default filerefs can be modified.
|
||||||
Based on http://stackoverflow.com/questions/13046116/using-sas-to-copy-a-text-file
|
Based on:
|
||||||
|
https://stackoverflow.com/questions/13046116/using-sas-to-copy-a-text-file
|
||||||
|
|
||||||
%mp_binarycopy(inloc="/home/me/blah.txt", outref=_webout)
|
%mp_binarycopy(inloc="/home/me/blah.txt", outref=_webout)
|
||||||
|
|
||||||
@param inloc full, quoted "path/and/filename.ext" of the object to be copied
|
To append to a file, use the mode option, eg:
|
||||||
@param outloc full, quoted "path/and/filename.ext" of object to be created
|
|
||||||
@param inref can override default input fileref to avoid naming clash
|
filename tmp1 temp;
|
||||||
@param outref an override default output fileref to avoid naming clash
|
filename tmp2 temp;
|
||||||
|
data _null_;
|
||||||
|
file tmp1;
|
||||||
|
put 'stacking';
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_binarycopy(inref=tmp1, outref=tmp2, mode=APPEND)
|
||||||
|
%mp_binarycopy(inref=tmp1, outref=tmp2, mode=APPEND)
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] inloc quoted "path/and/filename.ext" of the file to be copied
|
||||||
|
@param [out] outloc quoted "path/and/filename.ext" of the file to be created
|
||||||
|
@param [in] inref (____in) If provided, this fileref will take precedence over
|
||||||
|
the `inloc` parameter
|
||||||
|
@param [out] outref (____in) If provided, this fileref will take precedence
|
||||||
|
over the `outloc` parameter. It must already exist!
|
||||||
|
@param [in] mode (CREATE) Valid values:
|
||||||
|
@li CREATE - Create the file (even if it already exists)
|
||||||
|
@li APPEND - Append to the file (don't overwrite)
|
||||||
|
|
||||||
@returns nothing
|
@returns nothing
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@@ -23,20 +43,29 @@
|
|||||||
,outloc= /* full path and filename of object to be created */
|
,outloc= /* full path and filename of object to be created */
|
||||||
,inref=____in /* override default to use own filerefs */
|
,inref=____in /* override default to use own filerefs */
|
||||||
,outref=____out /* override default to use own filerefs */
|
,outref=____out /* override default to use own filerefs */
|
||||||
|
,mode=CREATE
|
||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
|
%local mod outmode;
|
||||||
|
%if &mode=APPEND %then %do;
|
||||||
|
%let mod=mod;
|
||||||
|
%let outmode='a';
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%let outmode='o';
|
||||||
|
%end;
|
||||||
/* these IN and OUT filerefs can point to anything */
|
/* these IN and OUT filerefs can point to anything */
|
||||||
%if &inref = ____in %then %do;
|
%if &inref = ____in %then %do;
|
||||||
filename &inref &inloc lrecl=1048576 ;
|
filename &inref &inloc lrecl=1048576 ;
|
||||||
%end;
|
%end;
|
||||||
%if &outref=____out %then %do;
|
%if &outref=____out %then %do;
|
||||||
filename &outref &outloc lrecl=1048576 ;
|
filename &outref &outloc lrecl=1048576 &mod;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
/* copy the file byte-for-byte */
|
/* copy the file byte-for-byte */
|
||||||
data _null_;
|
data _null_;
|
||||||
length filein 8 fileid 8;
|
length filein 8 fileid 8;
|
||||||
filein = fopen("&inref",'I',1,'B');
|
filein = fopen("&inref",'I',1,'B');
|
||||||
fileid = fopen("&outref",'O',1,'B');
|
fileid = fopen("&outref",&outmode,1,'B');
|
||||||
rec = '20'x;
|
rec = '20'x;
|
||||||
do while(fread(filein)=0);
|
do while(fread(filein)=0);
|
||||||
rc = fget(filein,rec,1);
|
rc = fget(filein,rec,1);
|
||||||
@@ -52,4 +81,4 @@
|
|||||||
%if &outref=____out %then %do;
|
%if &outref=____out %then %do;
|
||||||
filename &outref clear;
|
filename &outref clear;
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mp_binarycopy;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
%macro mp_cleancsv(in=NOTPROVIDED,out=NOTPROVIDED,qchar='22'x);
|
%macro mp_cleancsv(in=NOTPROVIDED,out=NOTPROVIDED,qchar='22'x);
|
||||||
%if "&in"="NOTPROVIDED" or "&out"="NOTPROVIDED" %then %do;
|
%if "&in"="NOTPROVIDED" or "&out"="NOTPROVIDED" %then %do;
|
||||||
%put %str(ERR)OR: Please provide valid input (&in) and output (&out) locations;
|
%put %str(ERR)OR: Please provide valid input (&in) & output (&out) locations;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
@@ -67,5 +67,5 @@
|
|||||||
else put inchar $char1.;
|
else put inchar $char1.;
|
||||||
end;
|
end;
|
||||||
run;
|
run;
|
||||||
%mend;
|
%mend mp_cleancsv;
|
||||||
/** @endcond */
|
/** @endcond */
|
||||||
@@ -64,4 +64,4 @@ data &outds;
|
|||||||
output;
|
output;
|
||||||
run;
|
run;
|
||||||
|
|
||||||
%mend;
|
%mend mp_createconstraints;
|
||||||
@@ -80,4 +80,4 @@ Usage:
|
|||||||
)
|
)
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mp_createwebservice;
|
||||||
|
|||||||
@@ -141,4 +141,4 @@ data &outds
|
|||||||
%end;
|
%end;
|
||||||
run;
|
run;
|
||||||
|
|
||||||
%mend;
|
%mend mp_csv2ds;
|
||||||
@@ -49,4 +49,4 @@ data &outds;
|
|||||||
end;
|
end;
|
||||||
run;
|
run;
|
||||||
|
|
||||||
%mend;
|
%mend mp_deleteconstraints;
|
||||||
69
base/mp_deletefolder.sas
Normal file
69
base/mp_deletefolder.sas
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief A macro to delete a directory
|
||||||
|
@details Will delete all folder content (including subfolder content) and
|
||||||
|
finally, the folder itself.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%let rootdir=%sysfunc(pathname(work))/demo;
|
||||||
|
%mf_mkdir(&rootdir)
|
||||||
|
%mf_mkdir(&rootdir/subdir)
|
||||||
|
%mf_mkdir(&rootdir/subdir/subsubdir)
|
||||||
|
data "&rootdir/subdir/example.sas7bdat";
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_deletefolder(&rootdir)
|
||||||
|
|
||||||
|
@param path Unquoted path to the folder to delete.
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_getuniquename.sas
|
||||||
|
@li mf_isdir.sas
|
||||||
|
@li mp_dirlist.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_deletefolder.test.sas
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_deletefolder(folder);
|
||||||
|
/* proceed if valid directory */
|
||||||
|
%if %mf_isdir(&folder)=1 %then %do;
|
||||||
|
|
||||||
|
/* prep temp table */
|
||||||
|
%local tempds;
|
||||||
|
%let tempds=%mf_getuniquename();
|
||||||
|
|
||||||
|
/* recursive directory listing */
|
||||||
|
%mp_dirlist(path=&folder,outds=work.&tempds, maxdepth=MAX)
|
||||||
|
|
||||||
|
/* sort descending level so can delete folder contents before folders */
|
||||||
|
proc sort data=work.&tempds;
|
||||||
|
by descending level;
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* ensure top level folder is removed at the end */
|
||||||
|
proc sql;
|
||||||
|
insert into work.&tempds set filepath="&folder";
|
||||||
|
|
||||||
|
/* delete everything */
|
||||||
|
data _null_;
|
||||||
|
set work.&tempds end=last;
|
||||||
|
length fref $8;
|
||||||
|
rc=filename(fref,filepath);
|
||||||
|
rc=fdelete(fref);
|
||||||
|
if rc then do;
|
||||||
|
msg=sysmsg();
|
||||||
|
put "&sysmacroname:" / rc= / msg= / filepath=;
|
||||||
|
end;
|
||||||
|
rc=filename(fref);
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* tidy up */
|
||||||
|
proc sql;
|
||||||
|
drop table work.&tempds;
|
||||||
|
|
||||||
|
%end;
|
||||||
|
%else %put &sysmacroname: &folder: is not a valid / accessible folder. ;
|
||||||
|
%mend mp_deletefolder;
|
||||||
@@ -3,20 +3,13 @@
|
|||||||
@brief Returns all files and subdirectories within a specified parent
|
@brief Returns all files and subdirectories within a specified parent
|
||||||
@details When used with getattrs=NO, is not OS specific (uses dopen / dread).
|
@details When used with getattrs=NO, is not OS specific (uses dopen / dread).
|
||||||
|
|
||||||
If getattrs=YES then the doptname / foptname functions are used to scan all
|
|
||||||
properties - any characters that are not valid in a SAS name (v7) are simply
|
|
||||||
stripped, and the table is transposed so theat each property is a column
|
|
||||||
and there is one file per row. An attempt is made to get all properties
|
|
||||||
whether a file or folder, but some files/folders cannot be accessed, and so
|
|
||||||
not all properties can / will be populated.
|
|
||||||
|
|
||||||
Credit for the rename approach:
|
Credit for the rename approach:
|
||||||
https://communities.sas.com/t5/SAS-Programming/SAS-Function-to-convert-string-to-Legal-SAS-Name/m-p/27375/highlight/true#M5003
|
https://communities.sas.com/t5/SAS-Programming/SAS-Function-to-convert-string-to-Legal-SAS-Name/m-p/27375/highlight/true#M5003
|
||||||
|
|
||||||
|
|
||||||
usage:
|
usage:
|
||||||
|
|
||||||
%mp_dirlist(path=/some/location,outds=myTable)
|
%mp_dirlist(path=/some/location, outds=myTable, maxdepth=MAX)
|
||||||
|
|
||||||
%mp_dirlist(outds=cwdfileprops, getattrs=YES)
|
%mp_dirlist(outds=cwdfileprops, getattrs=YES)
|
||||||
|
|
||||||
@@ -30,11 +23,19 @@
|
|||||||
X CMD) do please raise an issue!
|
X CMD) do please raise an issue!
|
||||||
|
|
||||||
|
|
||||||
@param path= for which to return contents
|
@param [in] path= for which to return contents
|
||||||
@param fref= Provide a DISK engine fileref as an alternative to PATH
|
@param [in] fref= Provide a DISK engine fileref as an alternative to PATH
|
||||||
@param outds= the output dataset to create
|
@param [in] maxdepth= (0) Set to a positive integer to indicate the level of
|
||||||
@param getattrs= YES/NO (default=NO). Uses doptname and foptname to return
|
subdirectory scan recursion - eg 3, to go `./3/levels/deep`. For unlimited
|
||||||
all attributes for each file / folder.
|
recursion, set to MAX.
|
||||||
|
@param [out] outds= the output dataset to create
|
||||||
|
@param [out] getattrs= (NO) If getattrs=YES then the doptname / foptname
|
||||||
|
functions are used to scan all properties - any characters that are not
|
||||||
|
valid in a SAS name (v7) are simply stripped, and the table is transposed
|
||||||
|
so theat each property is a column and there is one file per row. An
|
||||||
|
attempt is made to get all properties whether a file or folder, but some
|
||||||
|
files/folders cannot be accessed, and so not all properties can / will be
|
||||||
|
populated.
|
||||||
|
|
||||||
|
|
||||||
@returns outds contains the following variables:
|
@returns outds contains the following variables:
|
||||||
@@ -44,8 +45,15 @@
|
|||||||
- filename (just the file name)
|
- filename (just the file name)
|
||||||
- ext (.extension)
|
- ext (.extension)
|
||||||
- msg (system message if any issues)
|
- msg (system message if any issues)
|
||||||
|
- level (depth of folder)
|
||||||
- OS SPECIFIC variables, if <code>getattrs=</code> is used.
|
- OS SPECIFIC variables, if <code>getattrs=</code> is used.
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_dropmembers.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_dirlist.test.sas
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
**/
|
**/
|
||||||
@@ -54,11 +62,27 @@
|
|||||||
, fref=0
|
, fref=0
|
||||||
, outds=work.mp_dirlist
|
, outds=work.mp_dirlist
|
||||||
, getattrs=NO
|
, getattrs=NO
|
||||||
|
, maxdepth=0
|
||||||
|
, level=0 /* The level of recursion to perform. For internal use only. */
|
||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
%let getattrs=%upcase(&getattrs)XX;
|
%let getattrs=%upcase(&getattrs)XX;
|
||||||
|
|
||||||
data &outds (compress=no keep=file_or_folder filepath filename ext msg directory);
|
/* temp table */
|
||||||
length directory filepath $500 fref fref2 $8 file_or_folder $6 filename $80 ext $20 msg $200;
|
%local out_ds;
|
||||||
|
data;run;
|
||||||
|
%let out_ds=%str(&syslast);
|
||||||
|
|
||||||
|
/* drop main (top) table if it exists */
|
||||||
|
%if &level=0 %then %do;
|
||||||
|
%mp_dropmembers(%scan(&outds,-1,.), libref=WORK)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
data &out_ds(compress=no
|
||||||
|
keep=file_or_folder filepath filename ext msg directory level
|
||||||
|
);
|
||||||
|
length directory filepath $500 fref fref2 $8 file_or_folder $6 filename $80
|
||||||
|
ext $20 msg $200;
|
||||||
|
retain level &level;
|
||||||
%if &fref=0 %then %do;
|
%if &fref=0 %then %do;
|
||||||
rc = filename(fref, "&path");
|
rc = filename(fref, "&path");
|
||||||
%end;
|
%end;
|
||||||
@@ -116,8 +140,8 @@ data &outds (compress=no keep=file_or_folder filepath filename ext msg directory
|
|||||||
run;
|
run;
|
||||||
|
|
||||||
%if %substr(&getattrs,1,1)=Y %then %do;
|
%if %substr(&getattrs,1,1)=Y %then %do;
|
||||||
data &outds;
|
data &out_ds;
|
||||||
set &outds;
|
set &out_ds;
|
||||||
length infoname infoval $60 fref $8;
|
length infoname infoval $60 fref $8;
|
||||||
rc=filename(fref,filepath);
|
rc=filename(fref,filepath);
|
||||||
drop rc infoname fid i close fref;
|
drop rc infoname fid i close fref;
|
||||||
@@ -158,10 +182,35 @@ run;
|
|||||||
run;
|
run;
|
||||||
proc sort;
|
proc sort;
|
||||||
by filepath sasname;
|
by filepath sasname;
|
||||||
proc transpose data=&outds out=&outds(drop=_:);
|
proc transpose data=&out_ds out=&out_ds(drop=_:);
|
||||||
id sasname;
|
id sasname;
|
||||||
var infoval;
|
var infoval;
|
||||||
by filepath file_or_folder filename ext ;
|
by filepath file_or_folder filename ext ;
|
||||||
run;
|
run;
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
|
||||||
|
data &out_ds;
|
||||||
|
set &out_ds(where=(filepath ne ''));
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* update main table */
|
||||||
|
proc append base=&outds data=&out_ds;
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* recursive call */
|
||||||
|
%if &maxdepth>&level or &maxdepth=MAX %then %do;
|
||||||
|
data _null_;
|
||||||
|
set &out_ds;
|
||||||
|
where file_or_folder='folder';
|
||||||
|
code=cats('%nrstr(%mp_dirlist(path=',filepath,",outds=&outds"
|
||||||
|
,",getattrs=&getattrs,level=%eval(&level+1),maxdepth=&maxdepth))");
|
||||||
|
put code=;
|
||||||
|
call execute(code);
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
/* tidy up */
|
||||||
|
proc sql;
|
||||||
|
drop table &out_ds;
|
||||||
|
|
||||||
|
%mend mp_dirlist;
|
||||||
@@ -47,4 +47,4 @@
|
|||||||
%end;
|
%end;
|
||||||
as &outvar length=&varlen
|
as &outvar length=&varlen
|
||||||
from &libds;
|
from &libds;
|
||||||
%mend;
|
%mend mp_distinctfmtvalues;
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
/**
|
/**
|
||||||
@file
|
@file
|
||||||
@brief Drops tables / views (if they exist) without warnings in the log
|
@brief Drops tables / views (if they exist) without warnings in the log
|
||||||
@details
|
@details Useful for dropping tables when you're not sure they exist, or if
|
||||||
|
you are not sure whether they are a dataset or view. Also efficient for
|
||||||
|
dropping multiple tables / views.
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
proc sql;
|
proc sql;
|
||||||
create table data1 as select * from sashelp.class;
|
create table data1 as select * from sashelp.class;
|
||||||
create view view2 as select * from sashelp.class;
|
create view view2 as select * from sashelp.class;
|
||||||
%mp_dropmembers(list=data1 view2)
|
%mp_dropmembers(data1 view2, libref=WORK)
|
||||||
|
|
||||||
|
|
||||||
<h4> SAS Macros </h4>
|
<h4> SAS Macros </h4>
|
||||||
@li mf_isblank.sas
|
@li mf_isblank.sas
|
||||||
|
|
||||||
|
|
||||||
@param list space separated list of datasets / views
|
@param list space separated list of datasets / views, WITHOUT libref
|
||||||
@param libref= can only drop from a single library at a time
|
@param libref= (WORK) Note - you can only drop from a single library at a time
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
@@ -35,4 +39,4 @@
|
|||||||
delete &list;
|
delete &list;
|
||||||
delete &list /mtype=view;
|
delete &list /mtype=view;
|
||||||
run;
|
run;
|
||||||
%mend;
|
%mend mp_dropmembers;
|
||||||
@@ -2,29 +2,49 @@
|
|||||||
@file
|
@file
|
||||||
@brief Create a CARDS file from a SAS dataset.
|
@brief Create a CARDS file from a SAS dataset.
|
||||||
@details Uses dataset attributes to convert all data into datalines.
|
@details Uses dataset attributes to convert all data into datalines.
|
||||||
Running the generated file will rebuild the original dataset.
|
Running the generated file will rebuild the original dataset. Includes
|
||||||
|
support for large decimals, binary data, PROCESSED_DTTM columns, and
|
||||||
|
alternative encoding. If the input dataset is empty, the cards file will
|
||||||
|
still be created.
|
||||||
|
|
||||||
|
Additional support to generate a random sample and max rows.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
%mp_ds2cards(base_ds=sashelp.class
|
%mp_ds2cards(base_ds=sashelp.class
|
||||||
|
, tgt_ds=work.class
|
||||||
, cards_file= "C:\temp\class.sas"
|
, cards_file= "C:\temp\class.sas"
|
||||||
, maxobs=5)
|
, showlog=NO
|
||||||
|
, maxobs=5
|
||||||
|
)
|
||||||
|
|
||||||
TODO:
|
TODO:
|
||||||
- labelling the dataset
|
- labelling the dataset
|
||||||
- explicity setting a unix LF
|
- explicity setting a unix LF
|
||||||
- constraints / indexes etc
|
- constraints / indexes etc
|
||||||
|
|
||||||
@param [in] base_ds= Should be two level - eg work.blah. This is the table that
|
@param [in] base_ds= Should be two level - eg work.blah. This is the table
|
||||||
is converted to a cards file.
|
that is converted to a cards file.
|
||||||
@param [in] tgt_ds= Table that the generated cards file would create. Optional -
|
@param [in] tgt_ds= Table that the generated cards file would create.
|
||||||
if omitted, will be same as BASE_DS.
|
Optional - if omitted, will be same as BASE_DS.
|
||||||
@param [out] cards_file= Location in which to write the (.sas) cards file
|
@param [out] cards_file= ("%sysfunc(pathname(work))/cardgen.sas") Location in
|
||||||
@param [in] maxobs= to limit output to the first <code>maxobs</code> observations
|
which to write the (.sas) cards file
|
||||||
@param [in] showlog= whether to show generated cards file in the SAS log (YES/NO)
|
@param [in] maxobs= (max) To limit output to the first <code>maxobs</code>
|
||||||
@param [in] outencoding= provide encoding value for file statement (eg utf-8)
|
observations, enter an integer here.
|
||||||
@param [in] append= If NO then will rebuild the cards file if it already exists,
|
@param [in] random_sample= (NO) Set to YES to generate a random sample of
|
||||||
otherwise will append to it. Used by the mp_lib2cards.sas macro.
|
data. Can be quite slow.
|
||||||
|
@param [in] showlog= (YES) Whether to show generated cards file in the SAS
|
||||||
|
log. Valid values:
|
||||||
|
@li YES
|
||||||
|
@li NO
|
||||||
|
@param [in] outencoding= Provide encoding value for file statement (eg utf-8)
|
||||||
|
@param [in] append= (NO) If NO then will rebuild the cards file if it already
|
||||||
|
exists, otherwise will append to it. Used by the mp_lib2cards.sas macro.
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_lib2cards.sas
|
||||||
|
@li mp_ds2inserts.sas
|
||||||
|
@li mp_mdtablewrite.sas
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
@@ -41,7 +61,7 @@
|
|||||||
%local i setds nvars;
|
%local i setds nvars;
|
||||||
|
|
||||||
%if not %sysfunc(exist(&base_ds)) %then %do;
|
%if not %sysfunc(exist(&base_ds)) %then %do;
|
||||||
%put WARNING: &base_ds does not exist;
|
%put %str(WARN)ING: &base_ds does not exist;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
@@ -49,24 +69,26 @@
|
|||||||
%if (&tgt_ds = ) %then %let tgt_ds=&base_ds;
|
%if (&tgt_ds = ) %then %let tgt_ds=&base_ds;
|
||||||
%if %index(&tgt_ds,.)=0 %then %let tgt_ds=WORK.%scan(&base_ds,2,.);
|
%if %index(&tgt_ds,.)=0 %then %let tgt_ds=WORK.%scan(&base_ds,2,.);
|
||||||
%if ("&outencoding" ne "") %then %let outencoding=encoding="&outencoding";
|
%if ("&outencoding" ne "") %then %let outencoding=encoding="&outencoding";
|
||||||
%if ("&append" = "") %then %let append=;
|
%if ("&append" = "" or "&append" = "NO") %then %let append=;
|
||||||
%else %let append=mod;
|
%else %let append=mod;
|
||||||
|
|
||||||
/* get varcount */
|
/* get varcount */
|
||||||
%let nvars=0;
|
%let nvars=0;
|
||||||
proc sql noprint;
|
proc sql noprint;
|
||||||
select count(*) into: nvars from dictionary.columns
|
select count(*) into: nvars from dictionary.columns
|
||||||
where libname="%scan(%upcase(&base_ds),1)"
|
where upcase(libname)="%scan(%upcase(&base_ds),1)"
|
||||||
and memname="%scan(%upcase(&base_ds),2)";
|
and upcase(memname)="%scan(%upcase(&base_ds),2)";
|
||||||
%if &nvars=0 %then %do;
|
%if &nvars=0 %then %do;
|
||||||
%put WARNING: Dataset &base_ds has no variables! It will not be converted.;
|
%put %str(WARN)ING: Dataset &base_ds has no variables, will not be converted.;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
/* get indexes */
|
/* get indexes */
|
||||||
proc sort data=sashelp.vindex
|
proc sort
|
||||||
(where=(upcase(libname)="%scan(%upcase(&base_ds),1)"
|
data=sashelp.vindex(
|
||||||
and upcase(memname)="%scan(%upcase(&base_ds),2)"))
|
where=(upcase(libname)="%scan(%upcase(&base_ds),1)"
|
||||||
|
and upcase(memname)="%scan(%upcase(&base_ds),2)")
|
||||||
|
)
|
||||||
out=_data_;
|
out=_data_;
|
||||||
by indxname indxpos;
|
by indxname indxpos;
|
||||||
run;
|
run;
|
||||||
@@ -111,8 +133,8 @@ proc sql
|
|||||||
reset outobs=max;
|
reset outobs=max;
|
||||||
create table datalines1 as
|
create table datalines1 as
|
||||||
select name,type,length,varnum,format,label from dictionary.columns
|
select name,type,length,varnum,format,label from dictionary.columns
|
||||||
where libname="%upcase(%scan(&base_ds,1))"
|
where upcase(libname)="%upcase(%scan(&base_ds,1))"
|
||||||
and memname="%upcase(%scan(&base_ds,2))";
|
and upcase(memname)="%upcase(%scan(&base_ds,2))";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Due to long decimals cannot use best. format
|
Due to long decimals cannot use best. format
|
||||||
@@ -133,7 +155,18 @@ data datalines_2;
|
|||||||
,put(',name,',best32.-l)
|
,put(',name,',best32.-l)
|
||||||
,substrn(put(',name,',bestd32.-l),1
|
,substrn(put(',name,',bestd32.-l),1
|
||||||
,findc(put(',name,',bestd32.-l),"0","TBK")))');
|
,findc(put(',name,',bestd32.-l),"0","TBK")))');
|
||||||
else dataline=name;
|
/**
|
||||||
|
* binary data must be converted, to store in text format. It is identified
|
||||||
|
* by the presence of the $HEX keyword in the format.
|
||||||
|
*/
|
||||||
|
else if upcase(format)=:'$HEX' then
|
||||||
|
dataline=cats('put(trim(',name,'),',format,')');
|
||||||
|
/**
|
||||||
|
* There is no easy way to store line breaks in a cards file.
|
||||||
|
* To discuss this, use: https://github.com/sasjs/core/issues/80
|
||||||
|
* Removing all nonprintables with kw (keep writeable)
|
||||||
|
*/
|
||||||
|
else dataline=cats('compress(',name,', ,"kw")');
|
||||||
run;
|
run;
|
||||||
|
|
||||||
proc sql noprint;
|
proc sql noprint;
|
||||||
@@ -158,7 +191,8 @@ data _null_;
|
|||||||
|
|
||||||
|
|
||||||
/* Build input statement */
|
/* Build input statement */
|
||||||
if type='char' then type3=':$char.';
|
if upcase(format)=:'$HEX' then type3=':'!!format;
|
||||||
|
else if type='char' then type3=':$char.';
|
||||||
str2=put(name,$33.)||type3;
|
str2=put(name,$33.)||type3;
|
||||||
|
|
||||||
|
|
||||||
@@ -180,11 +214,12 @@ data _null_;
|
|||||||
file &cards_file. &outencoding lrecl=32767 termstr=nl &append;
|
file &cards_file. &outencoding lrecl=32767 termstr=nl &append;
|
||||||
length __attrib $32767;
|
length __attrib $32767;
|
||||||
if _n_=1 then do;
|
if _n_=1 then do;
|
||||||
put '/*******************************************************************';
|
put '/**';
|
||||||
put " Datalines for %upcase(%scan(&base_ds,2)) dataset ";
|
put ' @file';
|
||||||
put " Generated by %nrstr(%%)mp_ds2cards()";
|
put " @brief Datalines for %upcase(%scan(&base_ds,2)) dataset";
|
||||||
|
put " @details Generated by %nrstr(%%)mp_ds2cards()";
|
||||||
put " Available on github.com/sasjs/core";
|
put " Available on github.com/sasjs/core";
|
||||||
put '********************************************************************/';
|
put '**/';
|
||||||
put "data &tgt_ds &indexes;";
|
put "data &tgt_ds &indexes;";
|
||||||
put "attrib ";
|
put "attrib ";
|
||||||
%do i = 1 %to &nvars;
|
%do i = 1 %to &nvars;
|
||||||
@@ -208,7 +243,7 @@ data _null_;
|
|||||||
put 'run;';
|
put 'run;';
|
||||||
end;
|
end;
|
||||||
else do;
|
else do;
|
||||||
put "infile cards dsd delimiter=',';";
|
put "infile cards dsd;";
|
||||||
put "input ";
|
put "input ";
|
||||||
%do i = 1 %to &nvars.;
|
%do i = 1 %to &nvars.;
|
||||||
%if(%length(&&input_stmt_&i..)) %then
|
%if(%length(&&input_stmt_&i..)) %then
|
||||||
@@ -247,4 +282,4 @@ quit;
|
|||||||
%put NOTE-;%put NOTE-;
|
%put NOTE-;%put NOTE-;
|
||||||
%put NOTE- %sysfunc(dequote(&cards_file.));
|
%put NOTE- %sysfunc(dequote(&cards_file.));
|
||||||
%put NOTE-;%put NOTE-;
|
%put NOTE-;%put NOTE-;
|
||||||
%mend;
|
%mend mp_ds2cards;
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
%if not %sysfunc(exist(&ds)) %then %do;
|
%if not %sysfunc(exist(&ds)) %then %do;
|
||||||
%put WARNING: &ds does not exist;
|
%put %str(WARN)ING: &ds does not exist;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
@@ -55,4 +55,4 @@ data _null_;
|
|||||||
run;
|
run;
|
||||||
|
|
||||||
|
|
||||||
%mend;
|
%mend mp_ds2csv;
|
||||||
98
base/mp_ds2fmtds.sas
Normal file
98
base/mp_ds2fmtds.sas
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Converts every value in a dataset to it's formatted value
|
||||||
|
@details Converts every value to it's formatted value. All variables will
|
||||||
|
become character, and will be in the same order.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%mp_ds2fmtds(sashelp.cars,work.cars)
|
||||||
|
|
||||||
|
@param [in] libds The library.dataset to be converted
|
||||||
|
@param [out] outds The dataset to create.
|
||||||
|
|
||||||
|
<h4> Related Macros <h4>
|
||||||
|
@li mp_jsonout.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_ds2fmtds(libds, outds
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
/* validations */
|
||||||
|
%if not %sysfunc(exist(&libds)) %then %do;
|
||||||
|
%put %str(WARN)ING: &libds does not exist;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
%if %index(&libds,.)=0 %then %let libds=WORK.&libds;
|
||||||
|
|
||||||
|
/* grab metadata */
|
||||||
|
proc contents noprint data=&libds
|
||||||
|
out=_data_(keep=name type length format formatl formatd varnum);
|
||||||
|
run;
|
||||||
|
proc sort;
|
||||||
|
by varnum;
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* prepare formats and varnames */
|
||||||
|
data _null_;
|
||||||
|
set &syslast end=last;
|
||||||
|
name=upcase(name);
|
||||||
|
/* fix formats */
|
||||||
|
if type=2 or type=6 then do;
|
||||||
|
length fmt $49.;
|
||||||
|
if format='' then fmt=cats('$',length,'.');
|
||||||
|
else if formatl=0 then fmt=cats(format,'.');
|
||||||
|
else fmt=cats(format,formatl,'.');
|
||||||
|
newlen=max(formatl,length);
|
||||||
|
end;
|
||||||
|
else do;
|
||||||
|
if format='' then fmt='best.';
|
||||||
|
else if formatl=0 then fmt=cats(format,'.');
|
||||||
|
else if formatd=0 then fmt=cats(format,formatl,'.');
|
||||||
|
else fmt=cats(format,formatl,'.',formatd);
|
||||||
|
/* needs to be wide, for datetimes etc */
|
||||||
|
newlen=max(length,formatl,24);
|
||||||
|
end;
|
||||||
|
/* 32 char unique name */
|
||||||
|
newname='sasjs'!!substr(cats(put(md5(name),$hex32.)),1,27);
|
||||||
|
|
||||||
|
call symputx(cats('name',_n_),name,'l');
|
||||||
|
call symputx(cats('newname',_n_),newname,'l');
|
||||||
|
call symputx(cats('len',_n_),newlen,'l');
|
||||||
|
call symputx(cats('fmt',_n_),fmt,'l');
|
||||||
|
call symputx(cats('type',_n_),type,'l');
|
||||||
|
if last then call symputx('nobs',_n_,'l');
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* clean up */
|
||||||
|
proc sql;
|
||||||
|
drop table &syslast;
|
||||||
|
|
||||||
|
%if &nobs=0 %then %do;
|
||||||
|
%put Dataset &libds has no columns!
|
||||||
|
data &outds;
|
||||||
|
set &libds;
|
||||||
|
run;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
data &outds;
|
||||||
|
/* rename on entry */
|
||||||
|
set &libds(rename=(
|
||||||
|
%local i;
|
||||||
|
%do i=1 %to &nobs;
|
||||||
|
&&name&i=&&newname&i
|
||||||
|
%end;
|
||||||
|
));
|
||||||
|
%do i=1 %to &nobs;
|
||||||
|
length &&name&i $&&len&i;
|
||||||
|
&&name&i=left(put(&&newname&i,&&fmt&i));
|
||||||
|
drop &&newname&i;
|
||||||
|
%end;
|
||||||
|
if _error_ then call symputx('syscc',1012);
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mend mp_ds2fmtds;
|
||||||
179
base/mp_ds2inserts.sas
Normal file
179
base/mp_ds2inserts.sas
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Export a dataset to SQL insert statements
|
||||||
|
@details Converts dataset values to SQL insert statements for use across
|
||||||
|
multiple database types.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%mp_ds2inserts(sashelp.class,outref=myref,outds=class)
|
||||||
|
data class;
|
||||||
|
set sashelp.class;
|
||||||
|
stop;
|
||||||
|
proc sql;
|
||||||
|
%inc myref;
|
||||||
|
|
||||||
|
@param [in] ds The dataset to be exported
|
||||||
|
@param [in] maxobs= (max) The max number of inserts to create
|
||||||
|
@param [out] outref= (0) The output fileref. If it does not exist, it is
|
||||||
|
created. If it does exist, new records are APPENDED.
|
||||||
|
@param [out] schema= (0) The library (or schema) in which the target table is
|
||||||
|
located. If not provided, is ignored.
|
||||||
|
@param [out] outds= (0) The output table to load. If not provided, will
|
||||||
|
default to the table in the &ds parameter.
|
||||||
|
@param [in] flavour= (SAS) The SQL flavour to be applied to the output. Valid
|
||||||
|
options:
|
||||||
|
@li SAS (default) - suitable for regular proc sql
|
||||||
|
@li PGSQL - Used for Postgres databases
|
||||||
|
@param [in] applydttm= (YES) If YES, any columns using datetime formats will
|
||||||
|
be converted to native DB datetime literals
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_existfileref.sas
|
||||||
|
@li mf_getvarcount.sas
|
||||||
|
@li mf_getvarformat.sas
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mf_getvartype.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe (credit mjsq)
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_ds2inserts(ds, outref=0,schema=0,outds=0,flavour=SAS,maxobs=max
|
||||||
|
,applydttm=YES
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%if not %sysfunc(exist(&ds)) %then %do;
|
||||||
|
%put %str(WAR)NING: &ds does not exist;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%if not %sysfunc(exist(&ds)) %then %do;
|
||||||
|
%put %str(WAR)NING: &ds does not exist;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%if %index(&ds,.)=0 %then %let ds=WORK.&ds;
|
||||||
|
|
||||||
|
%let flavour=%upcase(&flavour);
|
||||||
|
%if &flavour ne SAS and &flavour ne PGSQL %then %do;
|
||||||
|
%put %str(WAR)NING: &flavour is not supported;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%if &outref=0 %then %do;
|
||||||
|
%put %str(WAR)NING: Please provide a fileref;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
%if %mf_existfileref(&outref)=0 %then %do;
|
||||||
|
filename &outref temp lrecl=66000;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%if &schema=0 %then %let schema=;
|
||||||
|
%else %let schema=&schema..;
|
||||||
|
|
||||||
|
%if &outds=0 %then %let outds=%scan(&ds,2,.);
|
||||||
|
|
||||||
|
%local nobs;
|
||||||
|
proc sql noprint;
|
||||||
|
select count(*) into: nobs TRIMMED from &ds;
|
||||||
|
%if &nobs=0 %then %do;
|
||||||
|
data _null_;
|
||||||
|
file &outref mod;
|
||||||
|
put "/* No rows found in &ds */";
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%local vars;
|
||||||
|
%let vars=%mf_getvarcount(&ds);
|
||||||
|
%if &vars=0 %then %do;
|
||||||
|
data _null_;
|
||||||
|
file &outref mod;
|
||||||
|
put "/* No columns found in &schema.&ds */";
|
||||||
|
run;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
%else %if &vars>1600 and &flavour=PGSQL %then %do;
|
||||||
|
data _null_;
|
||||||
|
file &fref mod;
|
||||||
|
put "/* &schema.&ds contains &vars vars */";
|
||||||
|
put "/* Postgres cannot handle tables with over 1600 vars */";
|
||||||
|
put "/* No inserts will be generated for this table */";
|
||||||
|
run;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%local varlist varlistcomma;
|
||||||
|
%let varlist=%mf_getvarlist(&ds);
|
||||||
|
%let varlistcomma=%mf_getvarlist(&ds,dlm=%str(,),quote=double);
|
||||||
|
|
||||||
|
/* next, export data */
|
||||||
|
data _null_;
|
||||||
|
file &outref mod ;
|
||||||
|
if _n_=1 then put "/* &schema.&outds (&nobs rows, &vars columns) */";
|
||||||
|
set &ds;
|
||||||
|
%if &maxobs ne max %then %do;
|
||||||
|
if _n_>&maxobs then stop;
|
||||||
|
%end;
|
||||||
|
length _____str $32767;
|
||||||
|
format _numeric_ best.;
|
||||||
|
format _character_ ;
|
||||||
|
%local i comma var vtype vfmt;
|
||||||
|
%do i=1 %to %sysfunc(countw(&varlist));
|
||||||
|
%let var=%scan(&varlist,&i);
|
||||||
|
%let vtype=%mf_getvartype(&ds,&var);
|
||||||
|
%let vfmt=%upcase(%mf_getvarformat(&ds,&var,force=1));
|
||||||
|
%if &i=1 %then %do;
|
||||||
|
%if &flavour=SAS %then %do;
|
||||||
|
put "insert into &schema.&outds set ";
|
||||||
|
put " &var="@;
|
||||||
|
%end;
|
||||||
|
%else %if &flavour=PGSQL %then %do;
|
||||||
|
_____str=cats(
|
||||||
|
"INSERT INTO &schema.&outds ("
|
||||||
|
,symget('varlistcomma')
|
||||||
|
,") VALUES ("
|
||||||
|
);
|
||||||
|
put _____str;
|
||||||
|
put " "@;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%if &flavour=SAS %then %do;
|
||||||
|
put " ,&var="@;
|
||||||
|
%end;
|
||||||
|
%else %if &flavour=PGSQL %then %do;
|
||||||
|
put " ,"@;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%if &vtype=N %then %do;
|
||||||
|
%if &flavour=SAS %then %do;
|
||||||
|
put &var;
|
||||||
|
%end;
|
||||||
|
%else %if &flavour=PGSQL %then %do;
|
||||||
|
if missing(&var) then put 'NULL';
|
||||||
|
%if &applydttm=YES and "%substr(&vfmt.xxxxxxxx,1,8)"="DATETIME"
|
||||||
|
%then %do;
|
||||||
|
else put "TIMESTAMP '" &var E8601DT25.6 "'";
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
else put &var;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
_____str="'"!!trim(tranwrd(&var,"'","''"))!!"'";
|
||||||
|
put _____str;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%if &flavour=SAS %then %do;
|
||||||
|
put ';';
|
||||||
|
%end;
|
||||||
|
%else %if &flavour=PGSQL %then %do;
|
||||||
|
put ');';
|
||||||
|
%end;
|
||||||
|
|
||||||
|
if _n_=&nobs then put /;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mend mp_ds2inserts;
|
||||||
203
base/mp_filtercheck.sas
Normal file
203
base/mp_filtercheck.sas
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Checks an input filter table for validity
|
||||||
|
@details Performs checks on the input table to ensure it arrives in the
|
||||||
|
correct format. This is necessary to prevent code injection. Will update
|
||||||
|
SYSCC to 1008 if bad records are found, and call mp_abort.sas for a
|
||||||
|
graceful service exit (configurable).
|
||||||
|
|
||||||
|
Used for dynamic filtering in [Data Controller for SAS®](https://datacontroller.io).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%mp_filtercheck(work.filter,targetds=sashelp.class,outds=work.badrecords)
|
||||||
|
|
||||||
|
The input table should have the following format:
|
||||||
|
|
||||||
|
|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$4000|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|AND|AND|1|AGE|=|12|
|
||||||
|
|AND|AND|1|SEX|<=|'M'|
|
||||||
|
|AND|OR|2|Name|NOT IN|('Jane','Alfred')|
|
||||||
|
|AND|OR|2|Weight|>=|7|
|
||||||
|
|
||||||
|
Rules applied:
|
||||||
|
|
||||||
|
@li GROUP_LOGIC - only AND/OR
|
||||||
|
@li SUBGROUP_LOGIC - only AND/OR
|
||||||
|
@li SUBGROUP_ID - only integers
|
||||||
|
@li VARIABLE_NM - must be in the target table
|
||||||
|
@li OPERATOR_NM - only =/>/</<=/>=/BETWEEN/IN/NOT IN/NE/CONTAINS
|
||||||
|
@li RAW_VALUE - no unquoted values except integers, commas and spaces.
|
||||||
|
|
||||||
|
@returns The &outds table containing any bad rows, plus a REASON_CD column.
|
||||||
|
|
||||||
|
@param [in] inds The table to be checked, with the format above
|
||||||
|
@param [in] targetds= The target dataset against which to verify VARIABLE_NM.
|
||||||
|
This must be available (ie, the library must be assigned).
|
||||||
|
@param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
|
||||||
|
@param [out] outds= The output table, which is a copy of the &inds. table
|
||||||
|
plus a REASON_CD column, containing only bad records. If bad records found,
|
||||||
|
the SYSCC value will be set to 1008 (general data problem). Downstream
|
||||||
|
processes should check this table (and return code) before continuing.
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mf_getuniquefileref.sas
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mf_getvartype.sas
|
||||||
|
@li mp_filtergenerate.sas
|
||||||
|
@li mp_filtervalidate.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_filtergenerate.sas
|
||||||
|
@li mp_filtervalidate.sas
|
||||||
|
|
||||||
|
@version 9.3
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
@todo Support date / hex / name literals and exponents in RAW_VALUE field
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_filtercheck(inds,targetds=,outds=work.badrecords,abort=YES);
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Validate input column */
|
||||||
|
%local vtype;
|
||||||
|
%let vtype=%mf_getvartype(&inds,RAW_VALUE);
|
||||||
|
%mp_abort(iftrue=(&abort=YES and &vtype ne C),
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(%str(ERR)OR: RAW_VALUE must be character)
|
||||||
|
)
|
||||||
|
%if &vtype ne C %then %do;
|
||||||
|
%put &sysmacroname: RAW_VALUE must be character;
|
||||||
|
%let syscc=42;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitise the values based on valid value lists, then strip out
|
||||||
|
* quotes, commas, periods and spaces.
|
||||||
|
* Only numeric values should remain
|
||||||
|
*/
|
||||||
|
%local reason_cd nobs;
|
||||||
|
%let nobs=0;
|
||||||
|
data &outds;
|
||||||
|
/*length GROUP_LOGIC SUBGROUP_LOGIC $3 SUBGROUP_ID 8 VARIABLE_NM $32
|
||||||
|
OPERATOR_NM $10 RAW_VALUE $4000;*/
|
||||||
|
set &inds;
|
||||||
|
length reason_cd $4032;
|
||||||
|
|
||||||
|
/* closed list checks */
|
||||||
|
if GROUP_LOGIC not in ('AND','OR') then do;
|
||||||
|
REASON_CD='GROUP_LOGIC should be AND/OR, not:'!!cats(GROUP_LOGIC);
|
||||||
|
putlog REASON_CD= GROUP_LOGIC=;
|
||||||
|
call symputx('reason_cd',reason_cd,'l');
|
||||||
|
call symputx('nobs',_n_,'l');
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if SUBGROUP_LOGIC not in ('AND','OR') then do;
|
||||||
|
REASON_CD='SUBGROUP_LOGIC should be AND/OR, not:'!!cats(SUBGROUP_LOGIC);
|
||||||
|
putlog REASON_CD= SUBGROUP_LOGIC=;
|
||||||
|
call symputx('reason_cd',reason_cd,'l');
|
||||||
|
call symputx('nobs',_n_,'l');
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if mod(SUBGROUP_ID,1) ne 0 then do;
|
||||||
|
REASON_CD='SUBGROUP_ID should be integer, not '!!left(subgroup_id);
|
||||||
|
putlog REASON_CD= SUBGROUP_ID=;
|
||||||
|
call symputx('reason_cd',reason_cd,'l');
|
||||||
|
call symputx('nobs',_n_,'l');
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if upcase(VARIABLE_NM) not in
|
||||||
|
(%upcase(%mf_getvarlist(&targetds,dlm=%str(,),quote=SINGLE)))
|
||||||
|
then do;
|
||||||
|
REASON_CD="Variable "!!cats(variable_nm)!!" not in &targetds";
|
||||||
|
putlog REASON_CD= VARIABLE_NM=;
|
||||||
|
call symputx('reason_cd',reason_cd,'l');
|
||||||
|
call symputx('nobs',_n_,'l');
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
if OPERATOR_NM not in
|
||||||
|
('=','>','<','<=','>=','BETWEEN','IN','NOT IN','NE','CONTAINS')
|
||||||
|
then do;
|
||||||
|
REASON_CD='Invalid OPERATOR_NM: '!!left(OPERATOR_NM);
|
||||||
|
putlog REASON_CD= OPERATOR_NM=;
|
||||||
|
call symputx('reason_cd',reason_cd,'l');
|
||||||
|
call symputx('nobs',_n_,'l');
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
|
||||||
|
/* special logic */
|
||||||
|
if OPERATOR_NM='BETWEEN' then raw_value1=tranwrd(raw_value,' AND ','');
|
||||||
|
else if OPERATOR_NM in ('IN','NOT IN') then do;
|
||||||
|
if substr(raw_value,1,1) ne '('
|
||||||
|
or substr(cats(reverse(raw_value)),1,1) ne ')'
|
||||||
|
then do;
|
||||||
|
REASON_CD='Missing start/end bracket in RAW_VALUE';
|
||||||
|
putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
|
||||||
|
call symputx('reason_cd',reason_cd,'l');
|
||||||
|
call symputx('nobs',_n_,'l');
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
else raw_value1=substr(raw_value,2,max(length(raw_value)-2,0));
|
||||||
|
end;
|
||||||
|
else raw_value1=raw_value;
|
||||||
|
|
||||||
|
/* remove nested literals eg '' */
|
||||||
|
raw_value1=tranwrd(raw_value1,"''",'');
|
||||||
|
|
||||||
|
/* now match string literals (always single quotes) */
|
||||||
|
raw_value2=raw_value1;
|
||||||
|
regex = prxparse("s/(\').*?(\')//");
|
||||||
|
call prxchange(regex,-1,raw_value2);
|
||||||
|
|
||||||
|
/* remove commas and periods*/
|
||||||
|
raw_value3=compress(raw_value2,',.');
|
||||||
|
|
||||||
|
/* output records that contain values other than digits and spaces */
|
||||||
|
if notdigit(compress(raw_value3,' '))>0 then do;
|
||||||
|
putlog raw_value3= $hex32.;
|
||||||
|
REASON_CD=cats('Invalid RAW_VALUE:',raw_value);
|
||||||
|
putlog REASON_CD= raw_value= raw_value1= raw_value2= raw_value3=;
|
||||||
|
call symputx('reason_cd',reason_cd,'l');
|
||||||
|
call symputx('nobs',_n_,'l');
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
|
||||||
|
run;
|
||||||
|
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
set &outds end=last;
|
||||||
|
putlog (_all_)(=);
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_abort(iftrue=(&abort=YES and &nobs>0),
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Data issue: %superq(reason_cd))
|
||||||
|
)
|
||||||
|
|
||||||
|
%if &nobs>0 %then %do;
|
||||||
|
%let syscc=1008;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* syntax checking passed but it does not mean the filter is valid
|
||||||
|
* for that we can run a proc sql validate query
|
||||||
|
*/
|
||||||
|
%local fref1;
|
||||||
|
%let fref1=%mf_getuniquefileref();
|
||||||
|
%mp_filtergenerate(&inds,outref=&fref1)
|
||||||
|
|
||||||
|
/* this macro will also set syscc to 1008 if any issues found */
|
||||||
|
%mp_filtervalidate(&fref1,&targetds,outds=&outds,abort=&abort)
|
||||||
|
|
||||||
|
%mend mp_filtercheck;
|
||||||
102
base/mp_filtergenerate.sas
Normal file
102
base/mp_filtergenerate.sas
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Generates a filter clause from an input table, to a fileref
|
||||||
|
@details Uses the input table to generate an output filter clause.
|
||||||
|
This feature is used to create dynamic dropdowns in [Data Controller for SAS®](
|
||||||
|
https://datacontroller.io). The input table should be in the format below:
|
||||||
|
|
||||||
|
|GROUP_LOGIC:$3|SUBGROUP_LOGIC:$3|SUBGROUP_ID:8.|VARIABLE_NM:$32|OPERATOR_NM:$10|RAW_VALUE:$4000|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|AND|AND|1|AGE|=|12|
|
||||||
|
|AND|AND|1|SEX|<=|'M'|
|
||||||
|
|AND|OR|2|Name|NOT IN|('Jane','Alfred')|
|
||||||
|
|AND|OR|2|Weight|>=|7|
|
||||||
|
|
||||||
|
Note - if the above table is received from an external client, the values
|
||||||
|
should first be validated using the mp_filtercheck.sas macro to avoid risk
|
||||||
|
of SQL injection.
|
||||||
|
|
||||||
|
To generate the filter, run the following code:
|
||||||
|
|
||||||
|
data work.filtertable;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$4000.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,AGE,=,12
|
||||||
|
AND,AND,1,SEX,<=,"'M'"
|
||||||
|
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
|
||||||
|
AND,OR,2,Weight,>=,7
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_filtergenerate(work.filtertable,outref=myfilter)
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
infile myfilter;
|
||||||
|
input;
|
||||||
|
put _infile_;
|
||||||
|
run;
|
||||||
|
|
||||||
|
Will write the following query to the log:
|
||||||
|
|
||||||
|
> (
|
||||||
|
> AGE = 12
|
||||||
|
> AND
|
||||||
|
> SEX <= 'M'
|
||||||
|
> ) AND (
|
||||||
|
> Name NOT IN ('Jane','Alfred')
|
||||||
|
> OR
|
||||||
|
> Weight >= 7
|
||||||
|
> )
|
||||||
|
|
||||||
|
@param [in] inds The input table with query values
|
||||||
|
@param [out] outref= The output fileref to contain the filter clause. Will
|
||||||
|
be created (or replaced).
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_filtercheck.sas
|
||||||
|
@li mp_filtervalidate.sas
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
|
||||||
|
@version 9.3
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_filtergenerate(inds,outref=filter);
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
filename &outref temp;
|
||||||
|
|
||||||
|
%if %mf_nobs(&inds)=0 %then %do;
|
||||||
|
/* ensure we have a default filter */
|
||||||
|
data _null_;
|
||||||
|
file &outref;
|
||||||
|
put '1=1';
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
data _null_;
|
||||||
|
file &outref lrecl=32800;
|
||||||
|
set &inds end=last;
|
||||||
|
by SUBGROUP_ID;
|
||||||
|
if _n_=1 then put '((';
|
||||||
|
else if first.SUBGROUP_ID then put +1 GROUP_LOGIC '(';
|
||||||
|
else put +2 SUBGROUP_LOGIC;
|
||||||
|
|
||||||
|
put +4 VARIABLE_NM OPERATOR_NM RAW_VALUE;
|
||||||
|
|
||||||
|
if last.SUBGROUP_ID then put ')'@;
|
||||||
|
if last then put ')';
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%mend mp_filtergenerate;
|
||||||
106
base/mp_filtervalidate.sas
Normal file
106
base/mp_filtervalidate.sas
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Checks a generated filter query for validity
|
||||||
|
@details Runs a generated filter in proc sql with the validate option.
|
||||||
|
Used in mp_filtercheck.sas in an fcmp container.
|
||||||
|
|
||||||
|
Built to support dynamic filtering in
|
||||||
|
[Data Controller for SAS®](https://datacontroller.io).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
data work.filtertable;
|
||||||
|
infile datalines4 dsd;
|
||||||
|
input GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32.
|
||||||
|
OPERATOR_NM:$10. RAW_VALUE:$4000.;
|
||||||
|
datalines4;
|
||||||
|
AND,AND,1,AGE,=,12
|
||||||
|
AND,AND,1,SEX,<=,"'M'"
|
||||||
|
AND,OR,2,Name,NOT IN,"('Jane','Alfred')"
|
||||||
|
AND,OR,2,Weight,>=,7
|
||||||
|
;;;;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_filtergenerate(work.filtertable,outref=myfilter)
|
||||||
|
|
||||||
|
%mp_filtervalidate(myfilter,sashelp.class)
|
||||||
|
|
||||||
|
|
||||||
|
@returns The SYSCC value will be 1008 if there are validation issues.
|
||||||
|
|
||||||
|
@param [in] inref The input fileref to validate (generated by
|
||||||
|
mp_filtergenerate.sas)
|
||||||
|
@param [in] targetds The target dataset against which to verify the query
|
||||||
|
@param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
|
||||||
|
@param [out] outds= (work.mp_filtervalidate) Output dataset containing the
|
||||||
|
error / warning message, if one exists. If this table contains any rows,
|
||||||
|
there are problems!
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_getuniquefileref.sas
|
||||||
|
@li mf_nobs.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_filtercheck.sas
|
||||||
|
@li mp_filtergenerate.sas
|
||||||
|
|
||||||
|
@version 9.3
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_filtervalidate(inref,targetds,abort=YES,outds=work.mp_filtervalidate);
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0 or &syserr ne 0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(syscc=&syscc / syserr=&syserr - on macro entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
%local fref1;
|
||||||
|
%let fref1=%mf_getuniquefileref();
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
file &fref1;
|
||||||
|
infile &inref end=eof;
|
||||||
|
if _n_=1 then do;
|
||||||
|
put "proc sql;";
|
||||||
|
put "validate select * from &targetds";
|
||||||
|
put "where " ;
|
||||||
|
end;
|
||||||
|
input;
|
||||||
|
put _infile_;
|
||||||
|
putlog _infile_;
|
||||||
|
if eof then put ";quit;";
|
||||||
|
run;
|
||||||
|
|
||||||
|
%inc &fref1;
|
||||||
|
|
||||||
|
data &outds;
|
||||||
|
if &sqlrc or &syscc or &syserr then do;
|
||||||
|
REASON_CD='VALIDATION_ERR'!!'OR: '!!
|
||||||
|
coalescec(symget('SYSERRORTEXT'),symget('SYSWARNINGTEXT'));
|
||||||
|
output;
|
||||||
|
end;
|
||||||
|
else stop;
|
||||||
|
run;
|
||||||
|
|
||||||
|
filename &fref1 clear;
|
||||||
|
|
||||||
|
%if %mf_nobs(&outds)>0 %then %do;
|
||||||
|
%if &abort=YES %then %do;
|
||||||
|
data _null_;
|
||||||
|
set &outds;
|
||||||
|
call symputx('REASON_CD',reason_cd,'l');
|
||||||
|
stop;
|
||||||
|
run;
|
||||||
|
%mp_abort(
|
||||||
|
mac=&sysmacroname,
|
||||||
|
msg=%str(Filter validation issues. ERR=%superq(SYSERRORTEXT)
|
||||||
|
, WARN=%superq(SYSWARNINGTEXT) )
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
%let syscc=1008;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%mend mp_filtervalidate;
|
||||||
65
base/mp_getcols.sas
Normal file
65
base/mp_getcols.sas
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Creates a dataset with column metadata.
|
||||||
|
@details This macro takes the `proc contents` output and "tidies it up" in the
|
||||||
|
following ways:
|
||||||
|
|
||||||
|
@li Blank labels are filled in with column names
|
||||||
|
@li Formats are reconstructed with default values
|
||||||
|
@li Types such as DATE / TIME / DATETIME are inferred from the formats
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
%mp_getcols(sashelp.airline,outds=work.myds)
|
||||||
|
|
||||||
|
@param ds The dataset from which to obtain column metadata
|
||||||
|
@param outds= (work.cols) The output dataset to create. Sample data:
|
||||||
|
|NAME $|LENGTH 8|VARNUM 8|LABEL $|FORMAT $49|TYPE $1 |DDTYPE $|
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
|AIR|8|2|international airline travel (thousands)|8.|N|NUMERIC|
|
||||||
|
|DATE|8|1|DATE|MONYY.|N|DATE|
|
||||||
|
|REGION|3|3|REGION|$3.|C|CHARACTER|
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mm_getcols.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_getcols(ds, outds=work.cols);
|
||||||
|
|
||||||
|
proc contents noprint data=&ds
|
||||||
|
out=_data_ (keep=name type length label varnum format:);
|
||||||
|
run;
|
||||||
|
data &outds(keep=name type length varnum format label ddtype);
|
||||||
|
set &syslast(rename=(format=format2 type=type2));
|
||||||
|
name=upcase(name);
|
||||||
|
if type2=2 then do;
|
||||||
|
length format $49.;
|
||||||
|
if format2='' then format=cats('$',length,'.');
|
||||||
|
else if formatl=0 then format=cats(format2,'.');
|
||||||
|
else format=cats(format2,formatl,'.');
|
||||||
|
type='C';
|
||||||
|
ddtype='CHARACTER';
|
||||||
|
end;
|
||||||
|
else do;
|
||||||
|
if format2='' then format=cats(length,'.');
|
||||||
|
else if formatl=0 then format=cats(format2,'.');
|
||||||
|
else if formatd=0 then format=cats(format2,formatl,'.');
|
||||||
|
else format=cats(format2,formatl,'.',formatd);
|
||||||
|
type='N';
|
||||||
|
if format=:'DATETIME' then ddtype='DATETIME';
|
||||||
|
else if format=:'DATE' or format=:'DDMMYY' or format=:'MMDDYY'
|
||||||
|
or format=:'YYMMDD' or format=:'E8601DA' or format=:'B8601DA'
|
||||||
|
or format=:'MONYY'
|
||||||
|
then ddtype='DATE';
|
||||||
|
else if format=:'TIME' then ddtype='TIME';
|
||||||
|
else ddtype='NUMERIC';
|
||||||
|
end;
|
||||||
|
if label='' then label=name;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mend mp_getcols;
|
||||||
@@ -39,22 +39,25 @@
|
|||||||
/* must use SQL as proc datasets does not support length changes */
|
/* must use SQL as proc datasets does not support length changes */
|
||||||
proc sql noprint;
|
proc sql noprint;
|
||||||
create table &outds as
|
create table &outds as
|
||||||
select a.TABLE_CATALOG as libref
|
select upcase(a.TABLE_CATALOG) as libref
|
||||||
,a.TABLE_NAME
|
,upcase(a.TABLE_NAME) as TABLE_NAME
|
||||||
,a.constraint_type
|
,a.constraint_type
|
||||||
,a.constraint_name
|
,a.constraint_name
|
||||||
,b.column_name
|
,b.column_name
|
||||||
from dictionary.TABLE_CONSTRAINTS a
|
from dictionary.TABLE_CONSTRAINTS a
|
||||||
left join dictionary.constraint_column_usage b
|
left join dictionary.constraint_column_usage b
|
||||||
on a.TABLE_CATALOG=b.TABLE_CATALOG
|
on upcase(a.TABLE_CATALOG)=upcase(b.TABLE_CATALOG)
|
||||||
and a.TABLE_NAME=b.TABLE_NAME
|
and upcase(a.TABLE_NAME)=upcase(b.TABLE_NAME)
|
||||||
and a.constraint_name=b.constraint_name
|
and a.constraint_name=b.constraint_name
|
||||||
where a.TABLE_CATALOG="&lib"
|
/**
|
||||||
and b.TABLE_CATALOG="&lib"
|
* We cannot apply this clause to the underlying dictionary table. See:
|
||||||
|
* https://communities.sas.com/t5/SAS-Programming/Unexpected-Where-Clause-behaviour-in-dictionary-TABLE/m-p/771554#M244867
|
||||||
|
*/
|
||||||
|
where calculated libref="&lib"
|
||||||
%if "&ds" ne "" %then %do;
|
%if "&ds" ne "" %then %do;
|
||||||
and a.TABLE_NAME="&ds"
|
and upcase(a.TABLE_NAME)="&ds"
|
||||||
and b.TABLE_NAME="&ds"
|
and upcase(b.TABLE_NAME)="&ds"
|
||||||
%end;
|
%end;
|
||||||
;
|
;
|
||||||
|
|
||||||
%mend;
|
%mend mp_getconstraints;
|
||||||
@@ -133,7 +133,9 @@ run;
|
|||||||
|
|
||||||
if notnull='yes' then notnul=' not null';
|
if notnull='yes' then notnul=' not null';
|
||||||
if notnull='no' and missing(label) then put ' ' name typ;
|
if notnull='no' and missing(label) then put ' ' name typ;
|
||||||
else if notnull='yes' and missing(label) then put ' ' name typ '[' notnul ']';
|
else if notnull='yes' and missing(label) then do;
|
||||||
|
put ' ' name typ '[' notnul ']';
|
||||||
|
end;
|
||||||
else if notnull='no' then put ' ' name typ '[' lab ']';
|
else if notnull='no' then put ' ' name typ '[' lab ']';
|
||||||
else put ' ' name typ '[' notnul ',' lab ']';
|
else put ' ' name typ '[' notnul ',' lab ']';
|
||||||
|
|
||||||
@@ -166,7 +168,7 @@ run;
|
|||||||
call symputx('constcheck',1);
|
call symputx('constcheck',1);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if last then call symputx('constraints_used',cats(upcase(constraints_used)));
|
if last then call symput('constraints_used',cats(upcase(constraints_used)));
|
||||||
|
|
||||||
length curds const col $39;
|
length curds const col $39;
|
||||||
curds="&curds";
|
curds="&curds";
|
||||||
@@ -176,7 +178,8 @@ run;
|
|||||||
|
|
||||||
proc append base=&pkds data=&syslast;run;
|
proc append base=&pkds data=&syslast;run;
|
||||||
|
|
||||||
/* Create Unique Indexes, but only if they were not already defined within the Constraints section. */
|
/* Create Unique Indexes, but only if they were not already defined within
|
||||||
|
the Constraints section. */
|
||||||
data _data_(keep=curds const col);
|
data _data_(keep=curds const col);
|
||||||
set &idxinfo (where=(
|
set &idxinfo (where=(
|
||||||
libname="%scan(&curds,1,.)"
|
libname="%scan(&curds,1,.)"
|
||||||
@@ -187,7 +190,7 @@ run;
|
|||||||
file &outref mod;
|
file &outref mod;
|
||||||
by idxusage indxname;
|
by idxusage indxname;
|
||||||
name=upcase(name);
|
name=upcase(name);
|
||||||
if &constcheck=1 then stop; /* in fact we only care about PKs so stop if we have */
|
if &constcheck=1 then stop; /* we only care about PKs so stop if we have */
|
||||||
if _n_=1 and &constcheck=0 then put / ' indexes {';
|
if _n_=1 and &constcheck=0 then put / ' indexes {';
|
||||||
|
|
||||||
length cols $5000;
|
length cols $5000;
|
||||||
@@ -261,7 +264,11 @@ run;
|
|||||||
line='Ref: "'!!"&curds"
|
line='Ref: "'!!"&curds"
|
||||||
!!cats('".(',"%mf_getquotedstr(&pkcols,dlm=%str(,),quote=%str( ))",')')
|
!!cats('".(',"%mf_getquotedstr(&pkcols,dlm=%str(,),quote=%str( ))",')')
|
||||||
!!' - '
|
!!' - '
|
||||||
!!cats(quote(trim(curds)),'.(',"%mf_getquotedstr(&pkcols,dlm=%str(,),quote=%str( ))",')');
|
!!cats(quote(trim(curds))
|
||||||
|
,'.('
|
||||||
|
,"%mf_getquotedstr(&pkcols,dlm=%str(,),quote=%str( ))"
|
||||||
|
,')'
|
||||||
|
);
|
||||||
put line;
|
put line;
|
||||||
run;
|
run;
|
||||||
|
|
||||||
@@ -282,7 +289,9 @@ run;
|
|||||||
create table &pkds.5b as
|
create table &pkds.5b as
|
||||||
select curds,count(*) as cnt
|
select curds,count(*) as cnt
|
||||||
from &pkds.5a
|
from &pkds.5a
|
||||||
where curds not in (select curds from &pkds.2 where cols="&pkcols") /* not a one to one match */
|
where curds not in (
|
||||||
|
select curds from &pkds.2 where cols="&pkcols"
|
||||||
|
) /* not a one to one match */
|
||||||
and curds ne "&curds" /* exclude self */
|
and curds ne "&curds" /* exclude self */
|
||||||
group by 1;
|
group by 1;
|
||||||
create table &pkds.6 as
|
create table &pkds.6 as
|
||||||
@@ -323,4 +332,4 @@ run;
|
|||||||
run;
|
run;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mp_getdbml;
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
to create tables in SAS or a database. The macro can be used at table or
|
to create tables in SAS or a database. The macro can be used at table or
|
||||||
library level. The default behaviour is to create DDL in SAS format.
|
library level. The default behaviour is to create DDL in SAS format.
|
||||||
|
|
||||||
|
Note - views are not currently supported.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
data test(index=(pk=(x y)/unique /nomiss));
|
data test(index=(pk=(x y)/unique /nomiss));
|
||||||
@@ -16,12 +18,14 @@
|
|||||||
%mp_getddl(work,test,flavour=tsql,showlog=YES)
|
%mp_getddl(work,test,flavour=tsql,showlog=YES)
|
||||||
|
|
||||||
<h4> SAS Macros </h4>
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_existfileref.sas
|
||||||
|
@li mf_getvarcount.sas
|
||||||
@li mp_getconstraints.sas
|
@li mp_getconstraints.sas
|
||||||
|
|
||||||
@param lib libref of the library to create DDL for. Should be assigned.
|
@param lib libref of the library to create DDL for. Should be assigned.
|
||||||
@param ds dataset to create ddl for (optional)
|
@param ds dataset to create ddl for (optional)
|
||||||
@param fref= the fileref to which to write the DDL. If not preassigned, will
|
@param fref= the fileref to which to _append_ the DDL. If it does not exist,
|
||||||
be assigned to TEMP.
|
it will be created.
|
||||||
@param flavour= The type of DDL to create (default=SAS). Supported=TSQL
|
@param flavour= The type of DDL to create (default=SAS). Supported=TSQL
|
||||||
@param showlog= Set to YES to show the DDL in the log
|
@param showlog= Set to YES to show the DDL in the log
|
||||||
@param schema= Choose a preferred schema name (default is to use actual schema
|
@param schema= Choose a preferred schema name (default is to use actual schema
|
||||||
@@ -37,9 +41,10 @@
|
|||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
/* check fileref is assigned */
|
/* check fileref is assigned */
|
||||||
%if %sysfunc(fileref(&fref)) > 0 %then %do;
|
%if %mf_existfileref(&fref)=0 %then %do;
|
||||||
filename &fref temp ;
|
filename &fref temp ;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%if %length(&libref)=0 %then %let libref=WORK;
|
%if %length(&libref)=0 %then %let libref=WORK;
|
||||||
%let flavour=%upcase(&flavour);
|
%let flavour=%upcase(&flavour);
|
||||||
|
|
||||||
@@ -47,6 +52,7 @@ proc sql noprint;
|
|||||||
create table _data_ as
|
create table _data_ as
|
||||||
select * from dictionary.tables
|
select * from dictionary.tables
|
||||||
where upcase(libname)="%upcase(&libref)"
|
where upcase(libname)="%upcase(&libref)"
|
||||||
|
and memtype='DATA' /* views not currently supported */
|
||||||
%if %length(&ds)>0 %then %do;
|
%if %length(&ds)>0 %then %do;
|
||||||
and upcase(memname)="%upcase(&ds)"
|
and upcase(memname)="%upcase(&ds)"
|
||||||
%end;
|
%end;
|
||||||
@@ -86,7 +92,9 @@ create table _data_ as
|
|||||||
%global constraints_used;
|
%global constraints_used;
|
||||||
data _null_;
|
data _null_;
|
||||||
length ctype $11 constraint_name_orig $256 constraints_used $5000;
|
length ctype $11 constraint_name_orig $256 constraints_used $5000;
|
||||||
set &colconst (where=(table_name="&curds" and constraint_type in ('PRIMARY','UNIQUE'))) end=last;
|
set &colconst(
|
||||||
|
where=(table_name="&curds" and constraint_type in ('PRIMARY','UNIQUE'))
|
||||||
|
) end=last;
|
||||||
file &fref mod;
|
file &fref mod;
|
||||||
by constraint_type constraint_name;
|
by constraint_type constraint_name;
|
||||||
retain constraints_used;
|
retain constraints_used;
|
||||||
@@ -113,10 +121,10 @@ create table _data_ as
|
|||||||
end;
|
end;
|
||||||
run;
|
run;
|
||||||
%put &=constraints_used;
|
%put &=constraints_used;
|
||||||
%mend;
|
%mend addConst;
|
||||||
|
|
||||||
data _null_;
|
data _null_;
|
||||||
file &fref;
|
file &fref mod;
|
||||||
put "/* DDL generated by &sysuserid on %sysfunc(datetime(),datetime19.) */";
|
put "/* DDL generated by &sysuserid on %sysfunc(datetime(),datetime19.) */";
|
||||||
run;
|
run;
|
||||||
|
|
||||||
@@ -139,13 +147,15 @@ run;
|
|||||||
put "create table &libref..&curds(";
|
put "create table &libref..&curds(";
|
||||||
end;
|
end;
|
||||||
else do;
|
else do;
|
||||||
|
/* just a placeholder - we filter out views at the top */
|
||||||
put "create view &libref..&curds(";
|
put "create view &libref..&curds(";
|
||||||
end;
|
end;
|
||||||
put " "@@;
|
put " "@@;
|
||||||
end;
|
end;
|
||||||
else put " ,"@@;
|
else put " ,"@@;
|
||||||
if length(format)>1 then fmt=" format="!!cats(format);
|
if length(format)>1 then fmt=" format="!!cats(format);
|
||||||
if length(label)>1 then lab=" label="!!quote(trim(label));
|
if length(label)>1 then
|
||||||
|
lab=" label="!!cats("'",tranwrd(label,"'","''"),"'");
|
||||||
if notnull='yes' then notnul=' not null';
|
if notnull='yes' then notnul=' not null';
|
||||||
if type='char' then typ=cats('char(',length,')');
|
if type='char' then typ=cats('char(',length,')');
|
||||||
else if length ne 8 then typ='num length='!!left(length);
|
else if length ne 8 then typ='num length='!!left(length);
|
||||||
@@ -161,10 +171,19 @@ run;
|
|||||||
put ');';
|
put ');';
|
||||||
run;
|
run;
|
||||||
|
|
||||||
/* Create Unique Indexes, but only if they were not already defined within the Constraints section. */
|
/* Create Unique Indexes, but only if they were not already defined within
|
||||||
|
the Constraints section. */
|
||||||
data _null_;
|
data _null_;
|
||||||
*length ds $128;
|
*length ds $128;
|
||||||
set &idxinfo (where=(memname="&curds" and unique='yes' and indxname not in (%sysfunc(tranwrd("&constraints_used",%str( ),%str(","))))));
|
set &idxinfo(
|
||||||
|
where=(
|
||||||
|
memname="&curds"
|
||||||
|
and unique='yes'
|
||||||
|
and indxname not in (
|
||||||
|
%sysfunc(tranwrd("&constraints_used",%str( ),%str(",")))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
file &fref mod;
|
file &fref mod;
|
||||||
by idxusage indxname;
|
by idxusage indxname;
|
||||||
/* ds=cats(libname,'.',memname); */
|
/* ds=cats(libname,'.',memname); */
|
||||||
@@ -192,7 +211,7 @@ run;
|
|||||||
proc sql noprint;
|
proc sql noprint;
|
||||||
select sysvalue into: schemaactual
|
select sysvalue into: schemaactual
|
||||||
from dictionary.libnames
|
from dictionary.libnames
|
||||||
where libname="&libref" and engine='SQLSVR';
|
where upcase(libname)="&libref" and engine='SQLSVR';
|
||||||
%let schema=%sysfunc(coalescec(&schemaactual,&schema,&libref));
|
%let schema=%sysfunc(coalescec(&schemaactual,&schema,&libref));
|
||||||
|
|
||||||
%do x=1 %to %sysfunc(countw(&dsnlist));
|
%do x=1 %to %sysfunc(countw(&dsnlist));
|
||||||
@@ -208,6 +227,7 @@ run;
|
|||||||
put "create table [&schema].[&curds](";
|
put "create table [&schema].[&curds](";
|
||||||
end;
|
end;
|
||||||
else do;
|
else do;
|
||||||
|
/* just a placeholder - we filter out views at the top */
|
||||||
put "create view [&schema].[&curds](";
|
put "create view [&schema].[&curds](";
|
||||||
end;
|
end;
|
||||||
put " "@@;
|
put " "@@;
|
||||||
@@ -228,10 +248,19 @@ run;
|
|||||||
/* Extra step for data constraints */
|
/* Extra step for data constraints */
|
||||||
%addConst()
|
%addConst()
|
||||||
|
|
||||||
/* Create Unique Indexes, but only if they were not already defined within the Constraints section. */
|
/* Create Unique Indexes, but only if they were not already defined within
|
||||||
|
the Constraints section. */
|
||||||
data _null_;
|
data _null_;
|
||||||
*length ds $128;
|
*length ds $128;
|
||||||
set &idxinfo (where=(memname="&curds" and unique='yes' and indxname not in (%sysfunc(tranwrd("&constraints_used",%str( ),%str(","))))));
|
set &idxinfo(
|
||||||
|
where=(
|
||||||
|
memname="&curds"
|
||||||
|
and unique='yes'
|
||||||
|
and indxname not in (
|
||||||
|
%sysfunc(tranwrd("&constraints_used",%str( ),%str(",")))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
file &fref mod;
|
file &fref mod;
|
||||||
by idxusage indxname;
|
by idxusage indxname;
|
||||||
*ds=cats(libname,'.',memname);
|
*ds=cats(libname,'.',memname);
|
||||||
@@ -275,13 +304,24 @@ run;
|
|||||||
proc sql noprint;
|
proc sql noprint;
|
||||||
select sysvalue into: schemaactual
|
select sysvalue into: schemaactual
|
||||||
from dictionary.libnames
|
from dictionary.libnames
|
||||||
where libname="&libref" and engine='POSTGRES';
|
where upcase(libname)="&libref" and engine='POSTGRES';
|
||||||
%let schema=%sysfunc(coalescec(&schemaactual,&schema,&libref));
|
%let schema=%sysfunc(coalescec(&schemaactual,&schema,&libref));
|
||||||
data _null_;
|
data _null_;
|
||||||
file &fref mod;
|
file &fref mod;
|
||||||
put "CREATE SCHEMA &schema;";
|
put "CREATE SCHEMA &schema;";
|
||||||
%do x=1 %to %sysfunc(countw(&dsnlist));
|
%do x=1 %to %sysfunc(countw(&dsnlist));
|
||||||
%let curds=%scan(&dsnlist,&x);
|
%let curds=%scan(&dsnlist,&x);
|
||||||
|
%local curdsvarcount;
|
||||||
|
%let curdsvarcount=%mf_getvarcount(&libref..&curds);
|
||||||
|
%if &curdsvarcount>1600 %then %do;
|
||||||
|
data _null_;
|
||||||
|
file &fref mod;
|
||||||
|
put "/* &libref..&curds contains &curdsvarcount vars */";
|
||||||
|
put "/* Postgres cannot create tables with over 1600 vars */";
|
||||||
|
put "/* No DDL will be generated for this table";
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
data _null_;
|
data _null_;
|
||||||
file &fref mod;
|
file &fref mod;
|
||||||
put "/* Postgres Flavour DDL for &schema..&curds */";
|
put "/* Postgres Flavour DDL for &schema..&curds */";
|
||||||
@@ -294,6 +334,7 @@ run;
|
|||||||
put "CREATE TABLE &schema..&curds (";
|
put "CREATE TABLE &schema..&curds (";
|
||||||
end;
|
end;
|
||||||
else do;
|
else do;
|
||||||
|
/* just a placeholder - we filter out views at the top */
|
||||||
put "CREATE VIEW &schema..&curds (";
|
put "CREATE VIEW &schema..&curds (";
|
||||||
end;
|
end;
|
||||||
put " "@@;
|
put " "@@;
|
||||||
@@ -320,24 +361,31 @@ run;
|
|||||||
put ');';
|
put ');';
|
||||||
run;
|
run;
|
||||||
|
|
||||||
/* Create Unique Indexes, but only if they were not already defined within the Constraints section. */
|
/* Create Unique Indexes, but only if they were not already defined within
|
||||||
|
the Constraints section. */
|
||||||
data _null_;
|
data _null_;
|
||||||
*length ds $128;
|
*length ds $128;
|
||||||
set &idxinfo (where=(memname="&curds" and unique='yes' and indxname not in (%sysfunc(tranwrd("&constraints_used",%str( ),%str(","))))));
|
set &idxinfo(
|
||||||
|
where=(
|
||||||
|
memname="&curds"
|
||||||
|
and unique='yes'
|
||||||
|
and indxname not in (
|
||||||
|
%sysfunc(tranwrd("&constraints_used",%str( ),%str(",")))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
file &fref mod;
|
file &fref mod;
|
||||||
by idxusage indxname;
|
by idxusage indxname;
|
||||||
/* ds=cats(libname,'.',memname); */
|
|
||||||
if first.indxname then do;
|
if first.indxname then do;
|
||||||
put 'CREATE UNIQUE INDEX "' indxname +(-1) '" ' "ON &schema..&curds(";
|
put 'CREATE UNIQUE INDEX "' indxname +(-1) '" ' "ON &schema..&curds(";
|
||||||
put ' "' name +(-1) '"' ;
|
put ' "' name +(-1) '"' ;
|
||||||
end;
|
end;
|
||||||
else put ' ,"' name +(-1) '"';
|
else put ' ,"' name +(-1) '"';
|
||||||
*else put ' ,' name ;
|
|
||||||
if last.indxname then do;
|
if last.indxname then do;
|
||||||
put ');';
|
put ');';
|
||||||
end;
|
end;
|
||||||
run;
|
run;
|
||||||
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%if %upcase(&showlog)=YES %then %do;
|
%if %upcase(&showlog)=YES %then %do;
|
||||||
@@ -349,4 +397,4 @@ run;
|
|||||||
run;
|
run;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mp_getddl;
|
||||||
@@ -8,10 +8,11 @@
|
|||||||
- NAME Name of the base dataset column
|
- NAME Name of the base dataset column
|
||||||
- MAXLEN Maximum length of the data contained therein.
|
- MAXLEN Maximum length of the data contained therein.
|
||||||
|
|
||||||
Character fields may be allocated very large widths (eg 32000) of which the maximum
|
Character fields may be allocated very large widths (eg 32000) of which the
|
||||||
value is likely to be much narrower. This macro was designed to enable a HTML
|
maximum value is likely to be much narrower. This macro was designed to
|
||||||
table to be appropriately sized however this could be used as part of a data
|
enable a HTML table to be appropriately sized however this could be used as
|
||||||
audit to ensure we aren't over-sizing our tables in relation to the data therein.
|
part of a data audit to ensure we aren't over-sizing our tables in relation to
|
||||||
|
the data therein.
|
||||||
|
|
||||||
Numeric fields are converted using the relevant format to determine the width.
|
Numeric fields are converted using the relevant format to determine the width.
|
||||||
Usage:
|
Usage:
|
||||||
@@ -69,4 +70,4 @@ create table &outds (rename=(
|
|||||||
out=&outds(rename=(_name_=NAME COL1=MAXLEN));
|
out=&outds(rename=(_name_=NAME COL1=MAXLEN));
|
||||||
run;
|
run;
|
||||||
|
|
||||||
%mend;
|
%mend mp_getmaxvarlengths;
|
||||||
53
base/mp_gsubfile.sas
Normal file
53
base/mp_gsubfile.sas
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Performs a text substitution on a file
|
||||||
|
@details Makes use of the GSUB function in LUA to perform a text substitution
|
||||||
|
in a file - either in-place, or writing to a new location. The benefit of
|
||||||
|
using LUA is that the entire file can be loaded into a single variable,
|
||||||
|
thereby side stepping the 32767 character limit in a data step.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%let file=%sysfunc(pathname(work))/file.txt;
|
||||||
|
%let str=replace/me;
|
||||||
|
%let rep=with/this;
|
||||||
|
data _null_;
|
||||||
|
file "&file";
|
||||||
|
put "&str";
|
||||||
|
run;
|
||||||
|
%mp_gsubfile(file=&file, patternvar=str, replacevar=rep)
|
||||||
|
data _null_;
|
||||||
|
infile "&file";
|
||||||
|
input;
|
||||||
|
list;
|
||||||
|
run;
|
||||||
|
|
||||||
|
@param file= (0) The file to perform the substitution on
|
||||||
|
@param patternvar= A macro variable containing the Lua
|
||||||
|
[pattern](https://www.lua.org/pil/20.2.html) to search for. Due to the use
|
||||||
|
of special (magic) characters in Lua patterns, it is safer to pass the NAME
|
||||||
|
of the macro variable containing the string, rather than the value itself.
|
||||||
|
@param replacevar= The name of the macro variable containing the replacement
|
||||||
|
_string_.
|
||||||
|
@param outfile= (0) The file to write the output to. If zero, then the file
|
||||||
|
is overwritten in-place.
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li ml_gsubfile.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_gsubfile.test.sas
|
||||||
|
|
||||||
|
@version 9.4
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_gsubfile(file=0,
|
||||||
|
patternvar=,
|
||||||
|
replacevar=,
|
||||||
|
outfile=0
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
%ml_gsubfile()
|
||||||
|
|
||||||
|
%mend mp_gsubfile;
|
||||||
@@ -301,4 +301,4 @@
|
|||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mp_guesspk;
|
||||||
77
base/mp_hashdataset.sas
Normal file
77
base/mp_hashdataset.sas
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Returns a unique hash for a dataset
|
||||||
|
@details Ignores metadata attributes, used only to hash values. Compared
|
||||||
|
datasets must be in the same order.
|
||||||
|
|
||||||
|
%mp_hashdataset(sashelp.class,outds=myhash)
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
set work.myhash;
|
||||||
|
put hashkey=;
|
||||||
|
run;
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_getattrn.sas
|
||||||
|
@li mf_getuniquename.sas
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mf_getvartype.sas
|
||||||
|
|
||||||
|
@param [in] libds dataset to hash
|
||||||
|
@param [in] salt= Provide a salt (could be, for instance, the dataset name)
|
||||||
|
@param [out] outds= (work.mf_hashdataset) The output dataset to create. This
|
||||||
|
will contain one column (hashkey) with one observation (a hex32.
|
||||||
|
representation of the input hash)
|
||||||
|
|hashkey:$32.|
|
||||||
|
|---|
|
||||||
|
|28ABC74ABFC45F50794237BA5566E6CA|
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_hashdataset(
|
||||||
|
libds,
|
||||||
|
outds=,
|
||||||
|
salt=
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
%if %mf_getattrn(&libds,NLOBS)=0 %then %do;
|
||||||
|
%put %str(WARN)ING: Dataset &libds is empty;, or is not a dataset;
|
||||||
|
%end;
|
||||||
|
%else %if %mf_getattrn(&libds,NLOBS)<0 %then %do;
|
||||||
|
%put %str(ERR)OR: Dataset &libds is not a dataset;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%local keyvar /* roll up the md5 */
|
||||||
|
prevkeyvar /* retain prev record md5 */
|
||||||
|
lastvar /* last var in input ds */
|
||||||
|
varlist var i;
|
||||||
|
/* avoid naming conflict for hash key vars */
|
||||||
|
%let keyvar=%mf_getuniquename();
|
||||||
|
%let prevkeyvar=%mf_getuniquename();
|
||||||
|
%let lastvar=%mf_getuniquename();
|
||||||
|
%let varlist=%mf_getvarlist(&libds);
|
||||||
|
data &outds(rename=(&keyvar=hashkey) keep=&keyvar);
|
||||||
|
length &prevkeyvar &keyvar $32;
|
||||||
|
retain &prevkeyvar "%sysfunc(md5(%str(&salt)),$hex32.)";
|
||||||
|
set &libds end=&lastvar;
|
||||||
|
/* hash should include previous row */
|
||||||
|
&keyvar=put(md5(&prevkeyvar
|
||||||
|
/* loop every column, hashing every individual value */
|
||||||
|
%do i=1 %to %sysfunc(countw(&varlist));
|
||||||
|
%let var=%scan(&varlist,&i,%str( ));
|
||||||
|
%if %mf_getvartype(&libds,&var)=C %then %do;
|
||||||
|
!!put(md5(trim(&var)),$hex32.)
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
!!put(md5(trim(put(&var*1,binary64.))),$hex32.)
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
),$hex32.);
|
||||||
|
&prevkeyvar=&keyvar;
|
||||||
|
if &lastvar then output;
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
%mend mp_hashdataset;
|
||||||
104
base/mp_include.sas
Normal file
104
base/mp_include.sas
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Performs a wrapped \%include
|
||||||
|
@details This macro wrapper is necessary if you need your included code to
|
||||||
|
know that it is being \%included.
|
||||||
|
|
||||||
|
If you are using %include in a regular program, you could make use of the
|
||||||
|
following macro variables:
|
||||||
|
|
||||||
|
@li SYSINCLUDEFILEDEVICE
|
||||||
|
@li SYSINCLUDEFILEDIR
|
||||||
|
@li SYSINCLUDEFILEFILEREF
|
||||||
|
@li SYSINCLUDEFILENAME
|
||||||
|
|
||||||
|
However these variables are NOT available inside a macro, as documented here:
|
||||||
|
https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/mcrolref/n1j5tcc0n2xczyn1kg1o0606gsv9.htm
|
||||||
|
|
||||||
|
This macro can be used in place of the %include statement, and will insert
|
||||||
|
the following (equivalent) global variables:
|
||||||
|
|
||||||
|
@li _SYSINCLUDEFILEDEVICE
|
||||||
|
@li _SYSINCLUDEFILEDIR
|
||||||
|
@li _SYSINCLUDEFILEFILEREF
|
||||||
|
@li _SYSINCLUDEFILENAME
|
||||||
|
|
||||||
|
These can be used whenever testing _within a macro_. Outside of the macro,
|
||||||
|
the regular automatic variables will still be available (thanks to a
|
||||||
|
concatenated file list in the include statement).
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
filename example temp;
|
||||||
|
data _null_;
|
||||||
|
file example;
|
||||||
|
put '%macro test();';
|
||||||
|
put '%put &=_SYSINCLUDEFILEFILEREF;';
|
||||||
|
put '%put &=SYSINCLUDEFILEFILEREF;';
|
||||||
|
put '%mend; %test()';
|
||||||
|
put '%put &=SYSINCLUDEFILEFILEREF;';
|
||||||
|
run;
|
||||||
|
%mp_include(example)
|
||||||
|
|
||||||
|
@param [in] fileref The fileref of the file to be included. Must be provided.
|
||||||
|
@param [in] prefix= (_) The prefix to apply to the global variables.
|
||||||
|
@param [in] opts= (SOURCE2) The options to apply to the %inc statement
|
||||||
|
@param [in] errds= (work.mp_abort_errds) There is no clean way to end a
|
||||||
|
process within a %include called within a macro. Furthermore, there is no
|
||||||
|
way to test if a macro is called within a %include. To handle this
|
||||||
|
particular scenario, the %mp_abort() macro will test for the existence of
|
||||||
|
the `_SYSINCLUDEFILEDEVICE` variable and return the outputs (msg,mac) inside
|
||||||
|
this dataset.
|
||||||
|
It will then run an abort cancel FILE to stop the include running, and pass
|
||||||
|
the dataset back.
|
||||||
|
NOTE - it is NOT possible to read this dataset as part of _this_ macro -
|
||||||
|
when running abort cancel FILE, ALL macros are closed, so instead it is
|
||||||
|
necessary to invoke "%mp_abort(mode=INCLUDE)" OUTSIDE of any macro wrappers.
|
||||||
|
|
||||||
|
|
||||||
|
@version 9.4
|
||||||
|
@author Allan Bowe
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_getuniquefileref.sas
|
||||||
|
@li mp_abort.sas
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_include(fileref
|
||||||
|
,prefix=_
|
||||||
|
,opts=SOURCE2
|
||||||
|
,errds=work.mp_abort_errds
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
/* prepare precode */
|
||||||
|
%local tempref;
|
||||||
|
%let tempref=%mf_getuniquefileref();
|
||||||
|
data _null_;
|
||||||
|
file &tempref;
|
||||||
|
set sashelp.vextfl(where=(fileref="%upcase(&fileref)"));
|
||||||
|
put '%let _SYSINCLUDEFILEDEVICE=' xengine ';';
|
||||||
|
name=scan(xpath,-1,'/\');
|
||||||
|
put '%let _SYSINCLUDEFILENAME=' name ';';
|
||||||
|
path=subpad(xpath,1,length(xpath)-length(name)-1);
|
||||||
|
put '%let _SYSINCLUDEFILEDIR=' path ';';
|
||||||
|
put '%let _SYSINCLUDEFILEFILEREF=' "&fileref;";
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* prepare the errds */
|
||||||
|
data &errds;
|
||||||
|
length msg mac $1000;
|
||||||
|
iftrue='1=0';
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* include the include */
|
||||||
|
%inc &tempref &fileref/&opts;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc ne 0)
|
||||||
|
,mac=%str(&_SYSINCLUDEFILEDIR/&_SYSINCLUDEFILENAME)
|
||||||
|
,msg=%str(syscc=&syscc after executing &_SYSINCLUDEFILENAME)
|
||||||
|
)
|
||||||
|
|
||||||
|
filename &tempref clear;
|
||||||
|
|
||||||
|
%mend mp_include;
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
@details PROC JSON is faster but will produce errs like the ones below if
|
@details PROC JSON is faster but will produce errs like the ones below if
|
||||||
special chars are encountered.
|
special chars are encountered.
|
||||||
|
|
||||||
|
> (ERR)OR: Some code points did not transcode.
|
||||||
|
|
||||||
> An object or array close is not valid at this point in the JSON text.
|
> An object or array close is not valid at this point in the JSON text.
|
||||||
|
|
||||||
> Date value out of range
|
> Date value out of range
|
||||||
|
|
||||||
If this happens, try running with ENGINE=DATASTEP.
|
If this happens, try running with ENGINE=DATASTEP.
|
||||||
@@ -14,7 +17,9 @@
|
|||||||
filename tmp temp;
|
filename tmp temp;
|
||||||
data class; set sashelp.class;run;
|
data class; set sashelp.class;run;
|
||||||
|
|
||||||
|
%mp_jsonout(OPEN,jref=tmp)
|
||||||
%mp_jsonout(OBJ,class,jref=tmp)
|
%mp_jsonout(OBJ,class,jref=tmp)
|
||||||
|
%mp_jsonout(CLOSE,jref=tmp)
|
||||||
|
|
||||||
data _null_;
|
data _null_;
|
||||||
infile tmp;
|
infile tmp;
|
||||||
@@ -27,22 +32,24 @@
|
|||||||
For more information see https://sasjs.io
|
For more information see https://sasjs.io
|
||||||
|
|
||||||
@param action Valid values:
|
@param action Valid values:
|
||||||
* OPEN - opens the JSON
|
@li OPEN - opens the JSON
|
||||||
* OBJ - sends a table with each row as an object
|
@li OBJ - sends a table with each row as an object
|
||||||
* ARR - sends a table with each row in an array
|
@li ARR - sends a table with each row in an array
|
||||||
* CLOSE - closes the JSON
|
@li CLOSE - closes the JSON
|
||||||
|
|
||||||
@param ds the dataset to send. Must be a work table.
|
@param ds the dataset to send. Must be a work table.
|
||||||
@param jref= the fileref to which to send the JSON
|
@param jref= the fileref to which to send the JSON
|
||||||
@param dslabel= the name to give the table in the exported JSON
|
@param dslabel= the name to give the table in the exported JSON
|
||||||
@param fmt= Whether to keep or strip formats from the table
|
@param fmt= Whether to keep or strip formats from the table
|
||||||
@param engine= Which engine to use to send the JSON, options are:
|
@param engine= Which engine to use to send the JSON, valid options are:
|
||||||
* PROCJSON (default)
|
@li PROCJSON (default)
|
||||||
* DATASTEP
|
@li DATASTEP (more reliable when data has non standard characters)
|
||||||
|
|
||||||
@param dbg= DEPRECATED - was used to conditionally add PRETTY to
|
@param dbg= DEPRECATED - was used to conditionally add PRETTY to
|
||||||
proc json but this can cause line truncation in large files.
|
proc json but this can cause line truncation in large files.
|
||||||
|
|
||||||
|
<h4> Related Macros <h4>
|
||||||
|
@li mp_ds2fmtds.sas
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
@@ -50,10 +57,11 @@
|
|||||||
|
|
||||||
**/
|
**/
|
||||||
|
|
||||||
%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=PROCJSON,dbg=0
|
%macro mp_jsonout(action,ds,jref=_webout,dslabel=,fmt=Y,engine=DATASTEP,dbg=0
|
||||||
)/*/STORE SOURCE*/;
|
)/*/STORE SOURCE*/;
|
||||||
%put output location=&jref;
|
%put output location=&jref;
|
||||||
%if &action=OPEN %then %do;
|
%if &action=OPEN %then %do;
|
||||||
|
options nobomfile;
|
||||||
data _null_;file &jref encoding='utf-8';
|
data _null_;file &jref encoding='utf-8';
|
||||||
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
|
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
|
||||||
run;
|
run;
|
||||||
@@ -81,9 +89,68 @@
|
|||||||
%put &sysmacroname: &ds NOT FOUND!!!;
|
%put &sysmacroname: &ds NOT FOUND!!!;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
data _null_;file &jref mod ;
|
%if &fmt=Y %then %do;
|
||||||
|
%put converting every variable to a formatted variable;
|
||||||
|
/* see mp_ds2fmtds.sas for source */
|
||||||
|
proc contents noprint data=&ds
|
||||||
|
out=_data_(keep=name type length format formatl formatd varnum);
|
||||||
|
run;
|
||||||
|
proc sort;
|
||||||
|
by varnum;
|
||||||
|
run;
|
||||||
|
%local fmtds;
|
||||||
|
%let fmtds=%scan(&syslast,2,.);
|
||||||
|
/* prepare formats and varnames */
|
||||||
|
data _null_;
|
||||||
|
if _n_=1 then call symputx('nobs',nobs,'l');
|
||||||
|
set &fmtds end=last nobs=nobs;
|
||||||
|
name=upcase(name);
|
||||||
|
/* fix formats */
|
||||||
|
if type=2 or type=6 then do;
|
||||||
|
length fmt $49.;
|
||||||
|
if format='' then fmt=cats('$',length,'.');
|
||||||
|
else if formatl=0 then fmt=cats(format,'.');
|
||||||
|
else fmt=cats(format,formatl,'.');
|
||||||
|
newlen=max(formatl,length);
|
||||||
|
end;
|
||||||
|
else do;
|
||||||
|
if format='' then fmt='best.';
|
||||||
|
else if formatl=0 then fmt=cats(format,'.');
|
||||||
|
else if formatd=0 then fmt=cats(format,formatl,'.');
|
||||||
|
else fmt=cats(format,formatl,'.',formatd);
|
||||||
|
/* needs to be wide, for datetimes etc */
|
||||||
|
newlen=max(length,formatl,24);
|
||||||
|
end;
|
||||||
|
/* 32 char unique name */
|
||||||
|
newname='sasjs'!!substr(cats(put(md5(name),$hex32.)),1,27);
|
||||||
|
|
||||||
|
call symputx(cats('name',_n_),name,'l');
|
||||||
|
call symputx(cats('newname',_n_),newname,'l');
|
||||||
|
call symputx(cats('len',_n_),newlen,'l');
|
||||||
|
call symputx(cats('fmt',_n_),fmt,'l');
|
||||||
|
call symputx(cats('type',_n_),type,'l');
|
||||||
|
run;
|
||||||
|
data &fmtds;
|
||||||
|
/* rename on entry */
|
||||||
|
set &ds(rename=(
|
||||||
|
%local i;
|
||||||
|
%do i=1 %to &nobs;
|
||||||
|
&&name&i=&&newname&i
|
||||||
|
%end;
|
||||||
|
));
|
||||||
|
%do i=1 %to &nobs;
|
||||||
|
length &&name&i $&&len&i;
|
||||||
|
&&name&i=left(put(&&newname&i,&&fmt&i));
|
||||||
|
drop &&newname&i;
|
||||||
|
%end;
|
||||||
|
if _error_ then call symputx('syscc',1012);
|
||||||
|
run;
|
||||||
|
%let ds=&fmtds;
|
||||||
|
%end; /* &fmt=Y */
|
||||||
|
data _null_;file &jref mod encoding='utf-8';
|
||||||
put "["; call symputx('cols',0,'l');
|
put "["; call symputx('cols',0,'l');
|
||||||
proc sort data=sashelp.vcolumn(where=(libname='WORK' & memname="%upcase(&ds)"))
|
proc sort
|
||||||
|
data=sashelp.vcolumn(where=(libname='WORK' & memname="%upcase(&ds)"))
|
||||||
out=_data_;
|
out=_data_;
|
||||||
by varnum;
|
by varnum;
|
||||||
|
|
||||||
@@ -122,7 +189,8 @@
|
|||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
run;
|
run;
|
||||||
/* write to temp loc to avoid _webout truncation - https://support.sas.com/kb/49/325.html */
|
/* write to temp loc to avoid _webout truncation
|
||||||
|
- https://support.sas.com/kb/49/325.html */
|
||||||
filename _sjs temp lrecl=131068 encoding='utf-8';
|
filename _sjs temp lrecl=131068 encoding='utf-8';
|
||||||
data _null_; file _sjs lrecl=131068 encoding='utf-8' mod;
|
data _null_; file _sjs lrecl=131068 encoding='utf-8' mod;
|
||||||
set &tempds;
|
set &tempds;
|
||||||
@@ -158,8 +226,8 @@
|
|||||||
%end;
|
%end;
|
||||||
|
|
||||||
%else %if &action=CLOSE %then %do;
|
%else %if &action=CLOSE %then %do;
|
||||||
data _null_;file &jref encoding='utf-8';
|
data _null_;file &jref encoding='utf-8' mod;
|
||||||
put "}";
|
put "}";
|
||||||
run;
|
run;
|
||||||
%end;
|
%end;
|
||||||
%mend;
|
%mend mp_jsonout;
|
||||||
|
|||||||
@@ -75,4 +75,4 @@ select distinct lowcase(memname)
|
|||||||
)
|
)
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mp_lib2cards;
|
||||||
77
base/mp_lib2inserts.sas
Normal file
77
base/mp_lib2inserts.sas
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Convert all data in a library to SQL insert statements
|
||||||
|
@details Gets list of members then calls the <code>%mp_ds2inserts()</code>
|
||||||
|
macro.
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%mp_getddl(sashelp, schema=work, fref=tempref)
|
||||||
|
|
||||||
|
%mp_lib2inserts(sashelp, schema=work, outref=tempref)
|
||||||
|
|
||||||
|
%inc tempref;
|
||||||
|
|
||||||
|
|
||||||
|
The output will be one file in the outref fileref.
|
||||||
|
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_ds2inserts.sas
|
||||||
|
|
||||||
|
|
||||||
|
@param [in] lib Library in which to convert all datasets to inserts
|
||||||
|
@param [in] flavour= (SAS) The SQL flavour to be applied to the output. Valid
|
||||||
|
options:
|
||||||
|
@li SAS (default) - suitable for regular proc sql
|
||||||
|
@li PGSQL - Used for Postgres databases
|
||||||
|
@param [in] maxobs= (max) The max number of observations (per table) to create
|
||||||
|
@param [out] outref= Output fileref in which to create the insert statements.
|
||||||
|
If it exists, it will be appended to, otherwise it will be created.
|
||||||
|
@param [out] schema= (0) The schema of the target database, or the libref.
|
||||||
|
@param [in] applydttm= (YES) If YES, any columns using datetime formats will
|
||||||
|
be converted to native DB datetime literals
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_lib2inserts(lib
|
||||||
|
,flavour=SAS
|
||||||
|
,outref=0
|
||||||
|
,schema=0
|
||||||
|
,maxobs=max
|
||||||
|
,applydttm=YES
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
/* Find the tables */
|
||||||
|
%local x ds memlist;
|
||||||
|
proc sql noprint;
|
||||||
|
select distinct lowcase(memname)
|
||||||
|
into: memlist
|
||||||
|
separated by ' '
|
||||||
|
from dictionary.tables
|
||||||
|
where upcase(libname)="%upcase(&lib)"
|
||||||
|
and memtype='DATA'; /* exclude views */
|
||||||
|
|
||||||
|
|
||||||
|
%let flavour=%upcase(&flavour);
|
||||||
|
%if &flavour ne SAS and &flavour ne PGSQL %then %do;
|
||||||
|
%put %str(WAR)NING: &flavour is not supported;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
|
||||||
|
/* create the inserts */
|
||||||
|
%do x=1 %to %sysfunc(countw(&memlist));
|
||||||
|
%let ds=%scan(&memlist,&x);
|
||||||
|
%mp_ds2inserts(&lib..&ds
|
||||||
|
,outref=&outref
|
||||||
|
,schema=&schema
|
||||||
|
,outds=&ds
|
||||||
|
,flavour=&flavour
|
||||||
|
,maxobs=&maxobs
|
||||||
|
,applydttm=&applydttm
|
||||||
|
)
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%mend mp_lib2inserts;
|
||||||
255
base/mp_lockanytable.sas
Normal file
255
base/mp_lockanytable.sas
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Mechanism for locking tables to prevent parallel modifications
|
||||||
|
@details Uses a control table to enable ANY table to be locked for updates.
|
||||||
|
Only useful if every update uses the macro! Used heavily within
|
||||||
|
[Data Controller for SAS](https://datacontroller.io).
|
||||||
|
|
||||||
|
The underlying table is structured as per the MAKETABLE action.
|
||||||
|
|
||||||
|
@param [in] action The action to be performed. Valid values:
|
||||||
|
@li LOCK - Sets the lock flag, also confirms if a SAS lock is available
|
||||||
|
@li UNLOCK - Unlocks the table
|
||||||
|
@li MAKETABLE - creates the control table (ctl_ds)
|
||||||
|
@param [in] lib= (WORK) The libref of the table to lock. Should already be
|
||||||
|
assigned.
|
||||||
|
@param [in] ds= The dataset to lock
|
||||||
|
@param [in] ref= A meaningful reference to enable the lock to be traced. Max
|
||||||
|
length is 200 characters.
|
||||||
|
@param [out] ctl_ds= (0) The control table which controls the actual locking.
|
||||||
|
Should already be assigned and available.
|
||||||
|
@param [in] loops= (25) Number of times to check for a lock.
|
||||||
|
@param [in] loop_secs= (1) Seconds to wait between each lock attempt
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mp_lockfilecheck.sas
|
||||||
|
@li mf_getuser.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_lockanytable.test.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_lockanytable(
|
||||||
|
action
|
||||||
|
,lib= WORK
|
||||||
|
,ds=0
|
||||||
|
,ref=
|
||||||
|
,ctl_ds=0
|
||||||
|
,loops=25
|
||||||
|
,loop_secs=1
|
||||||
|
);
|
||||||
|
data _null_;
|
||||||
|
if _n_=1 then putlog "&sysmacroname entry vars:";
|
||||||
|
set sashelp.vmacro;
|
||||||
|
where scope="&sysmacroname";
|
||||||
|
put name '=' value;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&ds=0 and &action ne MAKETABLE)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(dataset was not provided)
|
||||||
|
)
|
||||||
|
%mp_abort(iftrue= (&ctl_ds=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Control dataset was not provided)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* set up lib & mac vars */
|
||||||
|
%let lib=%upcase(&lib);
|
||||||
|
%let ds=%upcase(&ds);
|
||||||
|
%let action=%upcase(&action);
|
||||||
|
%local user x trans msg abortme;
|
||||||
|
%let user=%mf_getuser();
|
||||||
|
%let abortme=0;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&action ne LOCK & &action ne UNLOCK & &action ne MAKETABLE)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(Invalid action (&action) provided)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* if an err condition exists, exit before we even begin */
|
||||||
|
%mp_abort(iftrue= (&syscc>0 and &action=LOCK)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(aborting due to syscc=&syscc on LOCK entry)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* do not bother locking work tables (else may affect all WORK libraries) */
|
||||||
|
%if (%upcase(&lib)=WORK or %str(&lib)=%str()) & &action ne MAKETABLE %then %do;
|
||||||
|
%put NOTE: WORK libraries will not be registered in the locking system.;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
/* do not proceed if no observations can be processed */
|
||||||
|
%mp_abort(iftrue= (%sysfunc(getoption(OBS))=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(options obs = 0. syserrortext=&syserrortext)
|
||||||
|
)
|
||||||
|
|
||||||
|
%if &ACTION=LOCK %then %do;
|
||||||
|
|
||||||
|
/* abort if a SAS lock is already in place, or cannot be applied */
|
||||||
|
%mp_lockfilecheck(&lib..&ds)
|
||||||
|
|
||||||
|
/* next, check there is a record for this table */
|
||||||
|
%local record_exists_check;
|
||||||
|
proc sql noprint;
|
||||||
|
select count(*) into: record_exists_check from &ctl_ds
|
||||||
|
where LOCK_LIB ="&lib" and LOCK_DS="&ds";
|
||||||
|
quit;
|
||||||
|
%if &syscc>0 %then %put syscc=&syscc sqlrc=&sqlrc;
|
||||||
|
%if &record_exists_check=0 %then %do;
|
||||||
|
data _null_;
|
||||||
|
putlog "&sysmacroname: adding record to lock table..";
|
||||||
|
run;
|
||||||
|
|
||||||
|
data ;
|
||||||
|
if 0 then set &ctl_ds;
|
||||||
|
LOCK_LIB ="&lib";
|
||||||
|
LOCK_DS="&ds";
|
||||||
|
LOCK_STATUS_CD='LOCKED';
|
||||||
|
LOCK_START_DTTM="%sysfunc(datetime(),E8601DT26.6)"dt;
|
||||||
|
LOCK_USER_NM="&user";
|
||||||
|
LOCK_PID="&sysjobid";
|
||||||
|
LOCK_REF="&ref";
|
||||||
|
output;stop;
|
||||||
|
run;
|
||||||
|
%let trans=&syslast;
|
||||||
|
proc append base=&ctl_ds data=&trans;
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
/* if record does exist, perform lock attempts */
|
||||||
|
%else %do x=1 %to &loops;
|
||||||
|
data _null_;
|
||||||
|
putlog "&sysmacroname: attempting lock (iteration &x) "@;
|
||||||
|
putlog "at %sysfunc(datetime(),datetime19.) ..";
|
||||||
|
run;
|
||||||
|
|
||||||
|
proc sql;
|
||||||
|
update &ctl_ds
|
||||||
|
set LOCK_STATUS_CD='LOCKED'
|
||||||
|
, LOCK_START_DTTM="%sysfunc(datetime(),E8601DT26.6)"dt
|
||||||
|
, LOCK_USER_NM="&user"
|
||||||
|
, LOCK_PID="&sysjobid"
|
||||||
|
, LOCK_REF="&ref"
|
||||||
|
where LOCK_LIB ="&lib" and LOCK_DS="&ds";
|
||||||
|
quit;
|
||||||
|
/**
|
||||||
|
* NOTE - occasionally SQL server will return an err code (deadlocked
|
||||||
|
* transaction). If so, ignore it, keep calm, and carry on..
|
||||||
|
*/
|
||||||
|
%if &syscc>0 %then %do;
|
||||||
|
data _null_;
|
||||||
|
putlog 'NOTE-' / 'NOTE-';
|
||||||
|
putlog "NOTE- &sysmacroname: Update failed. "@;
|
||||||
|
putlog "Resetting err conditions and re-attempting.";
|
||||||
|
putlog "NOTE- syscc=&syscc syserr=&syserr sqlrc=&sqlrc";
|
||||||
|
putlog 'NOTE-' / 'NOTE-';
|
||||||
|
run;
|
||||||
|
%let syscc=0;
|
||||||
|
%let sqlrc=0;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
/* now check if the record was successfully updated */
|
||||||
|
%local success_check;
|
||||||
|
proc sql noprint;
|
||||||
|
select count(*) into: success_check from &ctl_ds
|
||||||
|
where LOCK_LIB ="&lib" and LOCK_DS="&ds"
|
||||||
|
and LOCK_PID="&sysjobid" and LOCK_STATUS_CD='LOCKED';
|
||||||
|
quit;
|
||||||
|
%if &success_check=0 %then %do;
|
||||||
|
%if &x < &loops %then %do;
|
||||||
|
/* pause before next check */
|
||||||
|
data _null_;
|
||||||
|
putlog 'NOTE-' / 'NOTE-';
|
||||||
|
putlog "NOTE- &sysmacroname: table locked, waiting "@;
|
||||||
|
putlog "%sysfunc(sleep(&loop_inc)) seconds.. ";
|
||||||
|
putlog "NOTE- (iteration &x of &loops)";
|
||||||
|
putlog 'NOTE-' / 'NOTE-';
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%let msg=Unable to lock &lib..&ds via &ctl_ds after &loops attempts.\n
|
||||||
|
Please ask your administrator to investigate!;
|
||||||
|
%let abortme=1;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
data _null_;
|
||||||
|
putlog 'NOTE-' / 'NOTE-';
|
||||||
|
putlog "NOTE- &sysmacroname: Table &lib..&ds locked at "@
|
||||||
|
putlog " %sysfunc(datetime(),datetime19.) (iteration &x)"@;
|
||||||
|
putlog 'NOTE-' / 'NOTE-';
|
||||||
|
run;
|
||||||
|
%if &syscc>0 %then %do;
|
||||||
|
%put setting syscc(&syscc) back to 0;
|
||||||
|
%let syscc=0;
|
||||||
|
%end;
|
||||||
|
%let x=&loops; /* no more iterations needed */
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%else %if &ACTION=UNLOCK %then %do;
|
||||||
|
%local status;
|
||||||
|
proc sql noprint;
|
||||||
|
select LOCK_STATUS_CD into: status from &ctl_ds
|
||||||
|
where LOCK_LIB ="&lib" and LOCK_DS="&ds";
|
||||||
|
quit;
|
||||||
|
%if &syscc>0 %then %put syscc=&syscc sqlrc=&sqlrc;
|
||||||
|
%if &status=LOCKED %then %do;
|
||||||
|
data _null_;
|
||||||
|
putlog "&sysmacroname: unlocking &lib..&ds:";
|
||||||
|
run;
|
||||||
|
proc sql;
|
||||||
|
update &ctl_ds
|
||||||
|
set LOCK_STATUS_CD='UNLOCKED'
|
||||||
|
, LOCK_END_DTTM="%sysfunc(datetime(),E8601DT26.6)"dt
|
||||||
|
, LOCK_USER_NM="&user"
|
||||||
|
, LOCK_PID="&sysjobid"
|
||||||
|
, LOCK_REF="&ref"
|
||||||
|
where LOCK_LIB ="&lib" and LOCK_DS="&ds";
|
||||||
|
quit;
|
||||||
|
%end;
|
||||||
|
%else %if &status=UNLOCKED %then %do;
|
||||||
|
%put %str(WAR)NING: &lib..&ds is already unlocked!;
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%put NOTE: Unrecognised STATUS_CD (&status) in &ctl_ds;
|
||||||
|
%let abortme=1;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%else %if &action=MAKETABLE %then %do;
|
||||||
|
proc sql;
|
||||||
|
create table &ctl_ds(
|
||||||
|
lock_lib char(8),
|
||||||
|
lock_ds char(32),
|
||||||
|
lock_status_cd char(10) not null,
|
||||||
|
lock_user_nm char(100) not null ,
|
||||||
|
lock_ref char(200),
|
||||||
|
lock_pid char(10),
|
||||||
|
lock_start_dttm num format=E8601DT26.6,
|
||||||
|
lock_end_dttm num format=E8601DT26.6,
|
||||||
|
constraint pk_mp_lockanytable primary key(lock_lib,lock_ds));
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
%let msg=lock_anytable given unsupported action (&action);
|
||||||
|
%let abortme=1;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
/* catch errors - mp_abort must be called outside of a logic block */
|
||||||
|
%mp_abort(iftrue=(&abortme=1),
|
||||||
|
msg=%superq(msg),
|
||||||
|
mac=&sysmacroname
|
||||||
|
)
|
||||||
|
|
||||||
|
%exit_macro:
|
||||||
|
data _null_;
|
||||||
|
put "&sysmacroname: Exit vars: action=&action lib=&lib ds=&ds";
|
||||||
|
put " syscc=&syscc sqlrc=&sqlrc syserr=&syserr";
|
||||||
|
run;
|
||||||
|
%mend mp_lockanytable;
|
||||||
|
|
||||||
|
|
||||||
97
base/mp_lockfilecheck.sas
Normal file
97
base/mp_lockfilecheck.sas
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Aborts if a SAS lock file is in place, or if one cannot be applied.
|
||||||
|
@details Used in conjuction with the mp_lockanytable macro.
|
||||||
|
More info here: https://sasensei.com/flash/24
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
data work.test; a=1;run;
|
||||||
|
%mp_lockfilecheck(work.test)
|
||||||
|
|
||||||
|
@param [in] libds The libref.dataset for which to check the lock status
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_abort.sas
|
||||||
|
@li mf_getattrc.sas
|
||||||
|
|
||||||
|
<h4> Related Macros </h4>
|
||||||
|
@li mp_lockanytable.sas
|
||||||
|
@li mp_lockfilecheck.test.sas
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_lockfilecheck(
|
||||||
|
libds
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
if _n_=1 then putlog "&sysmacroname entry vars:";
|
||||||
|
set sashelp.vmacro;
|
||||||
|
where scope="&sysmacroname";
|
||||||
|
put name '=' value;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&syscc>0)
|
||||||
|
,mac=checklock.sas
|
||||||
|
,msg=Aborting with syscc=&syscc on entry.
|
||||||
|
)
|
||||||
|
%mp_abort(iftrue= (&libds=0)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%str(libds not provided)
|
||||||
|
)
|
||||||
|
|
||||||
|
%local msg lib ds;
|
||||||
|
%let lib=%upcase(%scan(&libds,1,.));
|
||||||
|
%let ds=%upcase(%scan(&libds,2,.));
|
||||||
|
|
||||||
|
/* do not proceed if no observations can be processed */
|
||||||
|
%let msg=options obs = 0. syserrortext=%superq(syserrortext);
|
||||||
|
%mp_abort(iftrue= (%sysfunc(getoption(OBS))=0)
|
||||||
|
,mac=checklock.sas
|
||||||
|
,msg=%superq(msg)
|
||||||
|
)
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
putlog "Checking engine & member type";
|
||||||
|
run;
|
||||||
|
%local engine memtype;
|
||||||
|
%let memtype=%mf_getattrc(&libds,MTYPE);
|
||||||
|
%let engine=%mf_getattrc(&libds,ENGINE);
|
||||||
|
|
||||||
|
%if &engine ne V9 and &engine ne BASE %then %do;
|
||||||
|
data _null_;
|
||||||
|
putlog "Lib &lib is not assigned using BASE engine - uses &engine instead";
|
||||||
|
putlog "SAS lock check will not be performed";
|
||||||
|
run;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
%else %if &memtype ne DATA %then %do;
|
||||||
|
%put NOTE: Cannot lock a VIEW!! Memtype=&memtype;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
data _null_;
|
||||||
|
putlog "Engine = &engine, memtype=&memtype";
|
||||||
|
putlog "Attempting lock statement";
|
||||||
|
run;
|
||||||
|
|
||||||
|
lock &libds;
|
||||||
|
|
||||||
|
%local abortme;
|
||||||
|
%let abortme=0;
|
||||||
|
%if &syscc>0 or &SYSLCKRC ne 0 %then %do;
|
||||||
|
%let msg=Unable to apply lock on &libds (SYSLCKRC=&SYSLCKRC syscc=&syscc);
|
||||||
|
%put %str(ERR)OR: &sysmacroname: &msg;
|
||||||
|
%let abortme=1;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
lock &libds clear;
|
||||||
|
|
||||||
|
%mp_abort(iftrue= (&abortme=1)
|
||||||
|
,mac=&sysmacroname
|
||||||
|
,msg=%superq(msg)
|
||||||
|
)
|
||||||
|
|
||||||
|
%mend mp_lockfilecheck;
|
||||||
98
base/mp_mdtablewrite.sas
Normal file
98
base/mp_mdtablewrite.sas
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief Create a Markdown Table from a dataset
|
||||||
|
@details A markdown table is a simple table representation for use in
|
||||||
|
documents written in markdown format.
|
||||||
|
|
||||||
|
An online generator is available here:
|
||||||
|
https://www.tablesgenerator.com/markdown_tables
|
||||||
|
|
||||||
|
This structure is also used by the Macro Core library for documenting input/
|
||||||
|
output datasets, as well as the sasjs/cli tool for documenting inputs/outputs
|
||||||
|
for web services.
|
||||||
|
|
||||||
|
We take the standard definition one step further by embedding the informat
|
||||||
|
in the table header row, like so:
|
||||||
|
|
||||||
|
|var1:$32|var2:best.|var3:date9.|
|
||||||
|
|---|---|---|
|
||||||
|
|some text|42|01JAN1960|
|
||||||
|
|blah|1|31DEC1999|
|
||||||
|
|
||||||
|
Which resolves to:
|
||||||
|
|
||||||
|
|var1:$32|var2:best.|var3:date9.|
|
||||||
|
|---|---|---|
|
||||||
|
|some text|42|01JAN1960|
|
||||||
|
|blah|1|31DEC1999|
|
||||||
|
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
%mp_mdtablewrite(libds=sashelp.class,showlog=YES)
|
||||||
|
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mf_getvarlist.sas
|
||||||
|
@li mf_getvarformat.sas
|
||||||
|
|
||||||
|
@param [in] libds= the library / dataset to create or read from.
|
||||||
|
@param [out] fref= Fileref to contain the markdown. Default=mdtable.
|
||||||
|
@param [out] showlog= set to YES to show the markdown in the log. Default=NO.
|
||||||
|
|
||||||
|
@version 9.3
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mp_mdtablewrite(
|
||||||
|
libds=,
|
||||||
|
fref=mdtable,
|
||||||
|
showlog=NO
|
||||||
|
)/*/STORE SOURCE*/;
|
||||||
|
|
||||||
|
/* check fileref is assigned */
|
||||||
|
%if %sysfunc(fileref(&fref)) > 0 %then %do;
|
||||||
|
filename &fref temp;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%local vars;
|
||||||
|
%let vars=%mf_getvarlist(&libds);
|
||||||
|
|
||||||
|
/* create the header row */
|
||||||
|
data _null_;
|
||||||
|
file &fref;
|
||||||
|
length line $32767;
|
||||||
|
put '|'
|
||||||
|
%local i var fmt;
|
||||||
|
%do i=1 %to %sysfunc(countw(&vars));
|
||||||
|
%let var=%scan(&vars,&i);
|
||||||
|
%let fmt=%mf_getvarformat(&libds,&var,force=1);
|
||||||
|
"&var:&fmt|"
|
||||||
|
%end;
|
||||||
|
;
|
||||||
|
put '|'
|
||||||
|
%do i=1 %to %sysfunc(countw(&vars));
|
||||||
|
"---|"
|
||||||
|
%end;
|
||||||
|
;
|
||||||
|
run;
|
||||||
|
|
||||||
|
/* write out the data */
|
||||||
|
data _null_;
|
||||||
|
file &fref mod dlm='|' lrecl=32767;
|
||||||
|
set &libds ;
|
||||||
|
length line $32767;
|
||||||
|
line=cats('|',%mf_getvarlist(&libds,dlm=%str(,'|',)),'|');
|
||||||
|
put line;
|
||||||
|
run;
|
||||||
|
|
||||||
|
%if %upcase(&showlog)=YES %then %do;
|
||||||
|
options ps=max;
|
||||||
|
data _null_;
|
||||||
|
infile &fref;
|
||||||
|
input;
|
||||||
|
putlog _infile_;
|
||||||
|
run;
|
||||||
|
%end;
|
||||||
|
|
||||||
|
%mend mp_mdtablewrite;
|
||||||
@@ -39,4 +39,4 @@
|
|||||||
,dttm=%sysfunc(datetime());
|
,dttm=%sysfunc(datetime());
|
||||||
quit;
|
quit;
|
||||||
|
|
||||||
%mend;
|
%mend mp_perflog;
|
||||||
@@ -85,4 +85,4 @@
|
|||||||
"with record &record and " _n_=;
|
"with record &record and " _n_=;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mp_prevobs;
|
||||||
@@ -87,4 +87,4 @@ insert into &outds select distinct * from &append_ds;
|
|||||||
)
|
)
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
%mend;
|
%mend mp_recursivejoin;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
@file
|
@file
|
||||||
@brief Reset an option to original value
|
@brief Reset an option to original value
|
||||||
@details Inspired by the SAS Jedi - https://blogs.sas.com/content/sastraining/2012/08/14/jedi-sas-tricks-reset-sas-system-options/
|
@details Inspired by the SAS Jedi -
|
||||||
|
https://blogs.sas.com/content/sastraining/2012/08/14/jedi-sas-tricks-reset-sas-system-options
|
||||||
Called as follows:
|
Called as follows:
|
||||||
|
|
||||||
options obs=30;
|
options obs=30;
|
||||||
@@ -29,4 +30,4 @@ data _null_;
|
|||||||
end;
|
end;
|
||||||
run;
|
run;
|
||||||
|
|
||||||
%mend;
|
%mend mp_resetoption;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user